<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tushar Pamnani</title>
    <description>The latest articles on DEV Community by Tushar Pamnani (@tusharpamnani).</description>
    <link>https://dev.to/tusharpamnani</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3829510%2F330e44a6-8a3d-4ddf-89a8-9de840488ace.png</url>
      <title>DEV Community: Tushar Pamnani</title>
      <link>https://dev.to/tusharpamnani</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tusharpamnani"/>
    <language>en</language>
    <item>
      <title>45 Days Building on Midnight: An Honest Builder's POV</title>
      <dc:creator>Tushar Pamnani</dc:creator>
      <pubDate>Sat, 11 Apr 2026 18:39:26 +0000</pubDate>
      <link>https://dev.to/tusharpamnani/45-days-building-on-midnight-an-honest-builders-pov-355h</link>
      <guid>https://dev.to/tusharpamnani/45-days-building-on-midnight-an-honest-builders-pov-355h</guid>
      <description>&lt;p&gt;&lt;em&gt;If you're considering building on Midnight, read this before you start.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I want to be upfront about what this article is and isn't.&lt;/p&gt;

&lt;p&gt;It's not a tutorial. It's not a portfolio flex. It's what I actually experienced spending 45 days going full-time on Midnight - what broke me, what surprised me, what I built, and whether I'd do it again.&lt;/p&gt;

&lt;p&gt;The short answer is yes. The longer answer is more interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "45 Days Full-Time" Actually Means
&lt;/h2&gt;

&lt;p&gt;I was a final-year engineering student when I started. Full-time meant 2pm to 4-5am most nights, with weekend slots eaten by events and workshops. No structured hours, no team, no roadmap handed to me; just me, the docs, a lot of Discord messages, and a proof server running in Docker.&lt;/p&gt;

&lt;p&gt;I studied DeFi math, ZK cryptography, and Midnight's architecture simultaneously. I built while I learned. Some contracts took a day. Some took a week of debugging a single circuit constraint. By the end of 45 days I had built roughly 15 contracts and ~10 full-stack dApps.&lt;/p&gt;

&lt;p&gt;Here's what that actually looks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Escrow contract with ZK commitment schemes and state machine ordering&lt;/li&gt;
&lt;li&gt;Linear bonding curve token with ZK-verified quadratic reserve math&lt;/li&gt;
&lt;li&gt;pUSD v3 lending protocol (180+ tests, hit Midnight's block size limits, had to cut from 28 to 10 circuits)&lt;/li&gt;
&lt;li&gt;Prediction market with range-based rewards (Bet in Range: submitted to hackathon)&lt;/li&gt;
&lt;li&gt;Quadratic voting with ZK-verified sqrt weighting&lt;/li&gt;
&lt;li&gt;ZK allowlist with depth-20 Sparse Merkle Tree and domain-separated nullifiers&lt;/li&gt;
&lt;li&gt;LMSR prediction markets, NFT launchpad, coin flip dApp, gamified learning platform&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mn-scaffold&lt;/code&gt;: an npm CLI for scaffolding Midnight projects from a live template registry&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;midnight-wallet-kit&lt;/code&gt;: React hooks for Lace and 1AM wallet integration, published to npm&lt;/li&gt;
&lt;li&gt;Midnight Club: an open-source developer hub for the ecosystem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two contributions came directly out of this period that I'm particularly glad exist. The first: when Midnight SDK v8 landed with significant breaking changes, a new &lt;code&gt;WalletFacade.init&lt;/code&gt; pattern, mandatory encrypted private state, the &lt;code&gt;signRecipe&lt;/code&gt; methodology replacing manual intent signing, I'd already worked through all of it while upgrading my own projects. That understanding turned into a PR to Midnight's official &lt;code&gt;create-mn-app&lt;/code&gt; repository, upgrading the hello-world template to SDK v8. It's approved and pending merge. The second is already live: a documentation PR to the official &lt;code&gt;midnight-docs&lt;/code&gt; repo clarifying the &lt;code&gt;create-mn-app&lt;/code&gt; project selection flow, specifically documenting that the CLI prompts you to choose between a Contract or a Full dApp, and which templates are available for each. It's the kind of thing that seems obvious once you know it and completely opaque when you don't. Both came from the same place: hitting a wall, figuring it out, then making sure the next developer doesn't hit the same wall.&lt;/p&gt;

&lt;p&gt;I also got selected into the Midnight Aliit, the ecosystem's selective technical fellowship program for builders who contribute through code, education, and community. The name comes from Mandalorian: &lt;em&gt;aliit&lt;/em&gt; means "family" or "clan," which is an accurate description of what it actually feels like. Fellows are chosen based on prior contributions, demonstrated technical depth, and a track record of helping others build. It's not a badge; it comes with direct access to the teams building Midnight, the ability to feed product feedback directly upstream, and a responsibility to keep bringing new developers into the ecosystem. Getting in validated that the 45 days weren't just productive for me personally.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Thing That Made Me Cry: Developer Experience
&lt;/h2&gt;

&lt;p&gt;I'm going to say something that every Midnight developer is thinking but most won't write publicly: &lt;strong&gt;the DevEx is genuinely hard right now.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This isn't a complaint. It's a fact worth stating clearly for anyone evaluating whether to build here.&lt;/p&gt;

&lt;p&gt;There's no publicly hosted proof server. Every developer who wants to build, test, or demo anything that involves ZK proofs has to run the proof server locally in Docker. This is fine for development. It becomes a serious problem the moment you want to deploy something real at zero cost, the way you'd deploy a Solana program or an EVM contract and have users interact with it immediately. On Midnight, your users need a running proof server, or you need to run one for them, which isn't free.&lt;/p&gt;

&lt;p&gt;I built &lt;code&gt;midnight-wallet-kit&lt;/code&gt; specifically because there was no proper documentation for Lace provider APIs. I spent hours inspecting &lt;code&gt;window&lt;/code&gt; objects in the browser console to figure out what methods existed, what payloads they accepted, and how to make them work reliably across both wallets. There was no spec to read. I reverse-engineered it.&lt;/p&gt;

&lt;p&gt;The Compact compiler's error messages, at least in the versions I was working with, could be cryptic. Circuit constraints that fail don't always tell you &lt;em&gt;why&lt;/em&gt; clearly. You learn by developing an intuition for what the ZK constraint system will and won't accept, which takes time and pain.&lt;/p&gt;

&lt;p&gt;I hit Midnight's block size limits while building pUSD v3. Had to reduce the contract from 28 circuits down to 10. The documentation on block size constraints didn't exist at the time; I found the limit by hitting it.&lt;/p&gt;

&lt;p&gt;None of this made me want to stop. But it would make a lot of developers stop, and I've seen it happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Thing That Surprised Me: The Team Actually Shows Up
&lt;/h2&gt;

&lt;p&gt;I expected a new ecosystem with thin developer support. What I got was the opposite.&lt;/p&gt;

&lt;p&gt;Every time I got seriously stuck, and I mean &lt;em&gt;seriously&lt;/em&gt; stuck, the kind of stuck where you've been debugging for two days and you're considering starting over, someone from the Midnight team was in Discord helping me work through it. Not a bot. Not a "have you read the docs?" response. Actual engineers engaging with my specific problem.&lt;/p&gt;

&lt;p&gt;I know "good developer support" sounds like a bare minimum. In practice, it's not. Most chains at Midnight's stage have a Discord that's 90% price talk and 10% confused newcomers. Midnight's developer channels are genuinely technical. The people building the chain are in the same channels as the people building on it.&lt;/p&gt;

&lt;p&gt;This matters more than it sounds when you're working alone at 3am on a circuit constraint that won't compile.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Thing I Didn't Expect: ZK Stopped Being Scary
&lt;/h2&gt;

&lt;p&gt;I came into this with a genuine fear of zero-knowledge cryptography. I'd tried to implement ZK-adjacent things on other chains and made a mess every time.&lt;/p&gt;

&lt;p&gt;Midnight changed that.&lt;/p&gt;

&lt;p&gt;Not because Midnight made ZK easy, it didn't. But Compact forces you to engage with the actual concepts rather than letting you abstract over them. When the compiler rejects your program because you tried to move witness data to the public ledger without &lt;code&gt;disclose()&lt;/code&gt;, you don't just fix the error and move on. You understand &lt;em&gt;why&lt;/em&gt; it's an error. The language teaches you the mental model.&lt;/p&gt;

&lt;p&gt;By week three I was writing blog posts explaining ZK commitment schemes and the witness-verify pattern to other developers. By week five I was implementing Sparse Merkle Trees and domain-separated nullifiers. That's not knowledge I would have picked up passively. Midnight forced it.&lt;/p&gt;

&lt;p&gt;The DeFi math came the same way. Bonding curves required integrating a price function. LMSR prediction markets required understanding logarithmic scoring rules. pUSD required thinking about liquidation thresholds and collateral ratios under ZK constraints. I learned all of it because I had to implement it.&lt;/p&gt;

&lt;p&gt;If you come to Midnight for the privacy primitives, you'll leave with a significantly deeper understanding of cryptography than when you arrived. That's a real return on investment that doesn't show up in any ecosystem metric.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Missing: The Proof Server Problem Isn't Fully Solved
&lt;/h2&gt;

&lt;p&gt;Midnight does provide publicly hosted proof server endpoints for Preview, Preprod, and Mainnet. So technically, you don't &lt;em&gt;have&lt;/em&gt; to run one locally.&lt;/p&gt;

&lt;p&gt;But here's the thing the official docs say immediately after listing those endpoints: &lt;strong&gt;you should only use a proof server you control&lt;/strong&gt;, because the proof server receives your private data; token ownership details, private state, witness inputs. Using a hosted proof server you don't control means trusting a third party with exactly the data Midnight's privacy model is designed to protect.&lt;/p&gt;

&lt;p&gt;For development and testing: the hosted endpoints are convenient and fine. For a production dApp where privacy actually matters: you're back to running it yourself or self-hosting infrastructure. And for attracting users who aren't developers and won't run Docker, the UX friction is still real.&lt;/p&gt;

&lt;p&gt;This isn't a criticism of Midnight's architecture. The local proof server &lt;em&gt;is&lt;/em&gt; the right privacy model. But it creates a genuine tension: the chain's core value proposition is privacy, and the most privacy-respecting deployment requires infrastructure that most dApp users will never set up themselves. That tension doesn't have an easy answer, and I'd rather name it honestly than pretend the hosted endpoints make the problem disappear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should You Build on Midnight?
&lt;/h2&gt;

&lt;p&gt;The honest version of my answer: &lt;strong&gt;it depends on your risk tolerance and your grinding capacity.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you want a chain where you can build a polished dApp in a weekend and ship it to users immediately, Midnight is not that chain yet.&lt;/p&gt;

&lt;p&gt;If you're willing to grind through rough DevEx for a period of time in exchange for being early to a genuinely novel privacy primitive layer, the kind of early where there's almost no competition for the problems you're solving, then Midnight is worth serious consideration.&lt;/p&gt;

&lt;p&gt;The privacy problem in blockchain is real and unsolved. Midnight is one of the most technically interesting approaches to it. The team is building in public and engaging with developers. The ecosystem is small enough that good work gets noticed immediately, my escrow contract got into the official awesome-dapps list and onto the fireside dev hangout within 1 week of publishing.&lt;/p&gt;

&lt;p&gt;I would not have gotten that visibility shipping my 50th Solana DEX fork.&lt;/p&gt;

&lt;p&gt;If you're thinking about building on Midnight and want someone to walk you through the rough parts, I'm happy to help personally. I've hit most of the walls already.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;This retrospective is a checkpoint, not an ending. The repos stay open, the series stays up, &lt;code&gt;midnight-wallet-kit&lt;/code&gt; is on npm, and there's more to build.&lt;/p&gt;

&lt;p&gt;If you're starting your Midnight journey and want to skip some of the walls I hit, find me on &lt;a href="https://x.com/Tushar_Pamnani_" rel="noopener noreferrer"&gt;X&lt;/a&gt; or &lt;a href="https://github.com/tusharpamnani" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. The repos are open, the series is up, and the walls are documented.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://dev.to/tusharpamnani/series/37866"&gt;Midnight in Practice series&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/midnight-wallet-kit" rel="noopener noreferrer"&gt;midnight-wallet-kit&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/mn-scaffold" rel="noopener noreferrer"&gt;mn-scaffold&lt;/a&gt; · &lt;a href="https://midnight-club-zeta.vercel.app" rel="noopener noreferrer"&gt;Midnight Club&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>midnight</category>
      <category>web3</category>
      <category>blockchain</category>
      <category>developer</category>
    </item>
    <item>
      <title>I Spent Hours in the DOM So You Don't Have To</title>
      <dc:creator>Tushar Pamnani</dc:creator>
      <pubDate>Wed, 08 Apr 2026 20:57:19 +0000</pubDate>
      <link>https://dev.to/midnight-aliit/i-spent-hours-in-the-dom-so-you-dont-have-to-e8h</link>
      <guid>https://dev.to/midnight-aliit/i-spent-hours-in-the-dom-so-you-dont-have-to-e8h</guid>
      <description>&lt;p&gt;&lt;em&gt;Full source: &lt;a href="https://github.com/tusharpamnani/midnight-wallet-kit" rel="noopener noreferrer"&gt;github.com/tusharpamnani/midnight-wallet-kit&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When I started building frontends on Midnight, I hit a wall that nobody warned me about.&lt;/p&gt;

&lt;p&gt;The contracts were working. The proof server was running. The TypeScript SDK was integrated. And then I needed to connect a wallet.&lt;/p&gt;

&lt;p&gt;There was no proper documentation for Lace's Midnight provider. No fixed &lt;code&gt;window.ethereum&lt;/code&gt;-style standard to follow. Just two browser extensions injecting objects into the DOM under different keys, with different method signatures, inconsistent behavior, and no specification to read.&lt;/p&gt;

&lt;p&gt;I spent hours inspecting &lt;code&gt;window&lt;/code&gt; objects, console-logging provider payloads, and reverse-engineering what each wallet actually exposed before I could make a single connection work reliably. Every Midnight frontend developer hits this exact wall. Nobody should have to climb it twice.&lt;/p&gt;

&lt;p&gt;That's why I built &lt;code&gt;midnight-wallet-kit&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Problem Actually Looks Like
&lt;/h2&gt;

&lt;p&gt;If you've built on Ethereum, you know &lt;code&gt;window.ethereum&lt;/code&gt;. It's a standard. You call &lt;code&gt;eth_requestAccounts&lt;/code&gt;, you get addresses, you sign. Every wallet implements the same interface.&lt;/p&gt;

&lt;p&gt;Midnight doesn't have this yet. Lace injects under &lt;code&gt;window.lace&lt;/code&gt;. 1AM injects under &lt;code&gt;window.midnight&lt;/code&gt;. The methods exist but aren't documented. The payload shapes vary. And because Midnight uses ZK proofs and shielded transactions, the signing flow is more complex than EVM; you're not just signing a hash, you're signing an &lt;em&gt;intent&lt;/em&gt; that the proof server will process.&lt;/p&gt;

&lt;p&gt;Without a proper abstraction layer, every developer ends up writing the same fragile, bespoke integration code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// What you end up writing without a kit&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;lace&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;midnight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Lace not found?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;accounts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enable&lt;/span&gt;&lt;span class="p"&gt;?.();&lt;/span&gt;
&lt;span class="c1"&gt;// hope this works, documentation doesn't exist&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you do the same thing for 1AM. Then you handle the case where neither is installed. Then you handle session persistence across page refreshes. Then you write tests that require a real browser extension to be installed. By the time you're done, you've written a wallet library, except it only works for your specific app and breaks when the extension updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Midnight Wallet Kit Gives You
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;midnight-wallet-kit&lt;/code&gt; is a production-grade abstraction layer for Lace and 1AM on Midnight. Install it, register your adapters, wrap your app in a provider, and you're done.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;midnight-wallet-kit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;WalletManager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;InjectedWalletAdapter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;midnight-wallet-kit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;manager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WalletManager&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;manager&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InjectedWalletAdapter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Lace&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;providerKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lace&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InjectedWalletAdapter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1AM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;providerKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;midnight&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;WalletProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;midnight-wallet-kit/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;WalletProvider&lt;/span&gt;
      &lt;span class="na"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;manager&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;autoConnect&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1AM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Lace&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;// priority fallback order&lt;/span&gt;
      &lt;span class="na"&gt;autoRestore&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;              &lt;span class="c1"&gt;// reconnect last-used wallet on refresh&lt;/span&gt;
    &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;WalletProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire integration. Everything below this point is application code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Four Hooks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;useWallet()&lt;/code&gt;: connection state
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isConnected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;connectionState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useWallet&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;connectionState&lt;/code&gt; gives you the full lifecycle: &lt;code&gt;idle → connecting → connected&lt;/code&gt;, with &lt;code&gt;restoring&lt;/code&gt;, &lt;code&gt;error&lt;/code&gt;, &lt;code&gt;disconnecting&lt;/code&gt;, and &lt;code&gt;disconnected&lt;/code&gt; as intermediate states. No more boolean &lt;code&gt;isConnected&lt;/code&gt; that doesn't tell you &lt;em&gt;why&lt;/em&gt; a connection failed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;address&lt;/code&gt; returns the user's Midnight unshielded address — the same &lt;code&gt;mn_addr_...&lt;/code&gt; format used throughout the contract layer. &lt;code&gt;coinPublicKey&lt;/code&gt; and &lt;code&gt;encryptionPublicKey&lt;/code&gt; are also available if your dApp needs them.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;useConnect()&lt;/code&gt;: connection management
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;adapters&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useConnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Connect to a specific wallet&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1AM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Or let the kit try in priority order&lt;/span&gt;
&lt;span class="c1"&gt;// (handled automatically via WalletProvider autoConnect)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;adapters&lt;/code&gt; gives you the list of all registered adapter names; useful for rendering a wallet selection UI without hardcoding names.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;useIntent()&lt;/code&gt;: signing
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;buildAndSign&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signMessage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useIntent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Sign a contract intent&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;buildAndSign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;contractAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;circuitId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;buy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxCost&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Sign an arbitrary message (login flows, proofs of ownership)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;signMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Login to My DApp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Timestamping and normalization handled automatically&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;buildAndSign&lt;/code&gt; validates intent parameters with Zod before sending anything to the wallet; you get a typed &lt;code&gt;InvalidIntentError&lt;/code&gt; before the extension even opens, not a cryptic failure deep in the proof pipeline.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;signMessage&lt;/code&gt; handles the multi-step probing for data-signing support across different wallet versions, adds proper prefixes, and generates unique timestamps automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;useBalance()&lt;/code&gt;: balance polling
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refetch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useBalance&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// balance: { tDUST: bigint; shielded: bigint } | null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Automatically polls every 15 seconds when connected. &lt;code&gt;refetch()&lt;/code&gt; is available for manual triggers after a transaction. Uses the Midnight indexer under the hood; if the indexer query fails, you get a typed &lt;code&gt;BalanceFetchError&lt;/code&gt;, not a silent &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture: Why It's Built This Way
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Adapters normalize the provider chaos
&lt;/h3&gt;

&lt;p&gt;Every wallet integration lives in an adapter that implements &lt;code&gt;MidnightWallet&lt;/code&gt;. The &lt;code&gt;InjectedWalletAdapter&lt;/code&gt; handles the DOM-level provider probing, the part I spent hours figuring out manually. It exhaustively searches for working RPC methods across different provider standards and payload formats, so if Lace updates their injection key or 1AM changes their method signature, you update one adapter, not every component in your app.&lt;/p&gt;

&lt;p&gt;Wallet modes are explicitly typed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;intent-signing&lt;/code&gt;: supports the full &lt;code&gt;signIntent()&lt;/code&gt; flow, standard for DApps&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tx-only&lt;/code&gt;: direct transaction balancing and submission only&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;unknown&lt;/code&gt;: handled defensively as a fallback&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  WalletManager handles the lifecycle
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;WalletManager&lt;/code&gt; is the orchestrator. It manages adapter registration, connection state transitions, fallback chains, middleware, and session persistence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fallback chains&lt;/strong&gt; are the feature I wish I'd had from day one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connectWithFallback&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1AM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Lace&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try 1AM first. If it's not installed or the user rejects, try Lace. If both fail, throw &lt;code&gt;FallbackExhaustedError&lt;/code&gt;. One line. No manual try-catch chains.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Session persistence&lt;/strong&gt; via &lt;code&gt;autoRestore&lt;/code&gt; stores the last-connected wallet name in &lt;code&gt;localStorage&lt;/code&gt; and attempts to reconnect on page load. Users stay connected across refreshes without re-approving the extension every time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Middleware for observability
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Starting &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; on &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;adapterName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wallet_error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
      &lt;span class="na"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; 
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every wallet operation; connect, disconnect, signIntent, signMessage - passes through the middleware chain. The context object gives you the operation type, adapter name, intent payload, result, and any error. Useful for logging, analytics, and debugging production issues without adding instrumentation to every component.&lt;/p&gt;

&lt;h2&gt;
  
  
  Typed Errors: No More &lt;code&gt;catch (e: any)&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Every error from the kit is a typed class inheriting from &lt;code&gt;MidnightWalletError&lt;/code&gt;. You can branch on error type rather than parsing message strings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
  &lt;span class="nx"&gt;ProviderNotFoundError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;ConnectionRejectedError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;FallbackExhaustedError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;NetworkMismatchError&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;midnight-wallet-kit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Lace&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;ProviderNotFoundError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;showInstallPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lace&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;ConnectionRejectedError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;showRejectedMessage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;NetworkMismatchError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;showNetworkSwitchPrompt&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;NetworkMismatchError&lt;/code&gt; in particular is one you'll hit in the wild, if a user switches their wallet to mainnet while your dApp is pointed at preprod, the session breaks silently without this check. The kit detects and surfaces it explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Without a Browser Extension
&lt;/h2&gt;

&lt;p&gt;This is the part that usually breaks dApp test suites. Testing wallet integration normally requires a real browser extension to be installed and configured, which makes CI impossible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MockWalletAdapter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;midnight-wallet-kit/testing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MockWalletAdapter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TestWallet&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mn_addr1_test...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;coinPublicKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test_cpk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;signatureOverride&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0xmocksignature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;signDelay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// simulate realistic latency&lt;/span&gt;
  &lt;span class="na"&gt;shouldRejectSign&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can simulate connection failures, signing rejections, latency, and specific return values. No browser, no extension, no environment setup; just a mock that implements the same &lt;code&gt;MidnightWallet&lt;/code&gt; interface as the real adapters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Test the rejection flow&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;failAdapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MockWalletAdapter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RejectingWallet&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;shouldRejectConnect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RejectingWallet&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rejects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toThrow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ConnectionRejectedError&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  SSR and Next.js
&lt;/h2&gt;

&lt;p&gt;While building Midnight Club in Next.js, &lt;code&gt;window.lace&lt;/code&gt; and &lt;code&gt;window.midnight&lt;/code&gt; were being accessed during server-side rendering, causing &lt;code&gt;window is not defined&lt;/code&gt; crashes that were silent in development but broke production builds.&lt;/p&gt;

&lt;p&gt;The React hooks are SSR-safe. Provider detection (&lt;code&gt;window.lace&lt;/code&gt;, &lt;code&gt;window.midnight&lt;/code&gt;) only runs on the client; no &lt;code&gt;window is not defined&lt;/code&gt; errors during Next.js server-side rendering. &lt;code&gt;WalletProvider&lt;/code&gt; guards all DOM access behind a mounted check, so your app hydrates cleanly without injecting wallet state during SSR.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;The kit currently supports Lace and 1AM via &lt;code&gt;InjectedWalletAdapter&lt;/code&gt;. Hardware wallet support is the natural next step as the Midnight ecosystem matures and physical signing devices become available.&lt;/p&gt;

&lt;p&gt;If you're building on Midnight and hit something the kit doesn't handle, an edge case in the provider, a new wallet, a signing flow that doesn't fit the current API - issues and PRs are open at &lt;a href="https://github.com/tusharpamnani/midnight-wallet-kit" rel="noopener noreferrer"&gt;github.com/tusharpamnani/midnight-wallet-kit&lt;/a&gt;&lt;/p&gt;

</description>
      <category>midnight</category>
      <category>react</category>
      <category>typescript</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>ZK Membership Proofs on Midnight</title>
      <dc:creator>Tushar Pamnani</dc:creator>
      <pubDate>Thu, 02 Apr 2026 11:54:29 +0000</pubDate>
      <link>https://dev.to/midnight-aliit/zk-membership-proofs-on-midnight-1e29</link>
      <guid>https://dev.to/midnight-aliit/zk-membership-proofs-on-midnight-1e29</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 5 of Midnight in Practice. &lt;a href="https://dev.to/tusharpamnani/building-a-bonding-curve-token-on-midnight-with-real-zk-proofs-2h0i"&gt;Part 1&lt;/a&gt; | &lt;a href="https://dev.to/tusharpamnani/youre-probably-using-export-ledger-wrong-4j1c"&gt;Part 2&lt;/a&gt; | &lt;a href="https://dev.to/tusharpamnani/how-midnight-coordinates-two-party-transfers-3dfh"&gt;Part 3&lt;/a&gt; | &lt;a href="https://dev.toyour-part-4-url"&gt;Part 4&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Full source: &lt;a href="https://github.com/tusharpamnani/midnight-allowlist" rel="noopener noreferrer"&gt;github.com/tusharpamnani/midnight-allowlist&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every contract in this series has introduced one primitive that changes how you think about ZK development. The bonding curve introduced the witness-verify pattern. The escrow introduced commitment schemes for multi-party coordination. The QV contract introduced verification-over-computation for expensive arithmetic.&lt;/p&gt;

&lt;p&gt;This article introduces a different class of problem: &lt;strong&gt;proving membership in a set without revealing which member you are&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The allowlist contract solves it using a Sparse Merkle Tree; a data structure that lets anyone prove their leaf is included in a tree of up to one million members, using only twenty hash operations inside a ZK circuit, without revealing their leaf, their position, or their secret.&lt;/p&gt;

&lt;p&gt;By the end of this article you'll understand why Merkle trees are the standard structure for ZK membership proofs, how the circuit reconstructs a root from a private path, what nullifiers are doing cryptographically, and why the contract computes the nullifier inside the circuit rather than accepting it as a plain argument.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Membership Without Identity
&lt;/h2&gt;

&lt;p&gt;The naive approach to allowlisting is just a mapping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt; &lt;span class="n"&gt;allowlist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;allowlist&lt;/span&gt;&lt;span class="nf"&gt;.member&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="s"&gt;"Not allowed"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works, but it's fully public. Every allowed address is visible on-chain. Whoever calls &lt;code&gt;check&lt;/code&gt; reveals their address. For a token mint, an airdrop, or any access-gated system where the membership list itself is sensitive; a list of credentialed institutions, KYC-verified wallets, private beta testers, this model fails.&lt;/p&gt;

&lt;p&gt;What you want is: a user proves they are &lt;em&gt;in&lt;/em&gt; the allowlist without the chain learning &lt;em&gt;which member they are&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Merkle trees make this possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Merkle Trees Work for ZK Membership
&lt;/h2&gt;

&lt;p&gt;A Merkle tree is a binary tree where every leaf is a hash of some data, and every internal node is a hash of its two children. The root is a single 32-byte value that commits to the entire set.&lt;/p&gt;

&lt;p&gt;The key property: given any leaf, you can prove it's in the tree by providing the sibling hashes along the path from leaf to root; the &lt;em&gt;Merkle path&lt;/em&gt;. Anyone who knows the root can verify the proof by recomputing the path. Nobody learns anything about other leaves.&lt;/p&gt;

&lt;p&gt;For this allowlist, each leaf is &lt;code&gt;hash("zk-allowlist:leaf:v1" || secret)&lt;/code&gt;. The root is stored on-chain. The Merkle path and the secret stay entirely off-chain as witnesses. The circuit recomputes the root from the path and asserts it matches the on-chain value.&lt;/p&gt;

&lt;p&gt;The tree in this implementation has depth 20, supporting up to &lt;code&gt;2²⁰ = 1,048,576&lt;/code&gt; members. That's the range of the proof: one path, twenty sibling hashes, one root check.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ledger: Minimal Public Surface
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt; &lt;span class="n"&gt;merkle_root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt; &lt;span class="n"&gt;admin_commitment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt; &lt;span class="n"&gt;used_nullifiers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three fields. That's the entire public surface of this contract.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;merkle_root&lt;/code&gt; is the single commitment to the entire membership set. One 32-byte value represents up to a million members. Updating the set means updating one hash.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;admin_commitment&lt;/code&gt; is the governance primitive; a hash of the admin's secret credential. We'll cover this in detail below, but notice the design: the admin's identity is never on-chain. Only a commitment to their secret is. Authorization happens entirely inside a ZK proof.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;used_nullifiers&lt;/code&gt; is the replay protection set. Once a nullifier appears here, it can never be used again. The set grows with every successful &lt;code&gt;verifyAndUse&lt;/code&gt; call and is never pruned.&lt;/p&gt;

&lt;p&gt;Compare this to a naive allowlist that stores all member addresses: this contract reveals nothing about who is allowed. An observer sees a root, a commitment blob, and a set of 32-byte values with no meaning outside the context of a specific secret and context string.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Witnesses: Everything Private
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;witness&lt;/span&gt; &lt;span class="nf"&gt;getSecret&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;witness&lt;/span&gt; &lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;witness&lt;/span&gt; &lt;span class="nf"&gt;getSiblings&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;Vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;witness&lt;/span&gt; &lt;span class="nf"&gt;getPathIndices&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;Vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;witness&lt;/span&gt; &lt;span class="nf"&gt;getAdminSecret&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five witnesses. Four are for the membership proof, one is for governance.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;getSiblings()&lt;/code&gt; returns a &lt;code&gt;Vector&amp;lt;20, Bytes&amp;lt;32&amp;gt;&amp;gt;&lt;/code&gt;: the twenty sibling hashes along the Merkle path. In the TypeScript layer, these come from &lt;code&gt;tree.getMerklePath(leafIndex)&lt;/code&gt;. They are computed locally and fed into the proof server. They never appear on-chain.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;getPathIndices()&lt;/code&gt; returns &lt;code&gt;Vector&amp;lt;20, Boolean&amp;gt;&lt;/code&gt;: the direction bits. At each level, &lt;code&gt;false&lt;/code&gt; means the current node is a left child (sibling goes right), &lt;code&gt;true&lt;/code&gt; means it's a right child (sibling goes left). This determines which side each sibling hash goes in &lt;code&gt;hashLevelNode&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;getContext()&lt;/code&gt; is the scoping mechanism for nullifiers: more on this shortly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;getAdminSecret()&lt;/code&gt; is used only in &lt;code&gt;setRoot&lt;/code&gt;. The admin proves knowledge of the secret whose commitment matches &lt;code&gt;admin_commitment&lt;/code&gt; without ever disclosing the secret itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;hashLevelNode&lt;/code&gt; Circuit
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;hashLevelNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sibling&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_right&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;persistentHash&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="nf"&gt;pad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"zk-allowlist:node:v1"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;sibling&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;current&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;persistentHash&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="nf"&gt;pad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"zk-allowlist:node:v1"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;sibling&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the building block for the entire Merkle path reconstruction. It computes one level of the tree: given the current hash and its sibling, produce the parent hash.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;is_right&lt;/code&gt; flag controls which side the current node occupies. If the current node is a right child, the sibling goes left; so the hash is &lt;code&gt;H(domain || sibling || current)&lt;/code&gt;. If it's a left child, the hash is &lt;code&gt;H(domain || current || sibling)&lt;/code&gt;. Getting this wrong produces the wrong root and the assertion fails.&lt;/p&gt;

&lt;p&gt;The domain separator &lt;code&gt;"zk-allowlist:node:v1"&lt;/code&gt; is padded to 32 bytes and prepended to every node hash. This is the same domain separation philosophy from the QV nullifier discussion, it prevents a hash computed for one purpose from being confused with a hash computed for another. A leaf hash, a node hash, and a nullifier hash all use different tags even though they all call &lt;code&gt;persistentHash&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Vector&amp;lt;3, Bytes&amp;lt;32&amp;gt;&amp;gt;&lt;/code&gt; type annotation tells &lt;code&gt;persistentHash&lt;/code&gt; the exact structure of its input. This is the multi-argument pattern the QV contract couldn't use due to an earlier compiler constraint; by Compact 0.22 it works cleanly for vectors of fixed-length byte arrays.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;verifyAndUse&lt;/code&gt; Circuit, Step by Step
&lt;/h2&gt;

&lt;p&gt;This is the circuit that does the actual work. Let's walk through each of its six numbered steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Witness loading&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getSecret&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;siblings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getSiblings&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;path_indices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getPathIndices&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All four private inputs are loaded from witnesses. From this point forward, the circuit operates on these values without them ever being disclosed unless explicitly wrapped in &lt;code&gt;disclose()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Leaf computation&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;leaf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;persistentHash&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nf"&gt;pad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"zk-allowlist:leaf:v1"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;secret&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The member's leaf is derived from their secret inside the circuit. The domain tag &lt;code&gt;"zk-allowlist:leaf:v1"&lt;/code&gt; ensures a leaf hash is structurally different from a node hash even if the same secret were somehow used at both levels.&lt;/p&gt;

&lt;p&gt;Notice what doesn't happen here: the leaf is never &lt;code&gt;disclose()&lt;/code&gt;-d. It stays entirely within the witness data space of the proof. The chain never learns the leaf value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Nullifier verification&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;computed_nullifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;persistentHash&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nf"&gt;pad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"zk-allowlist:nullifier:v1"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;computed_nullifier&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;nullifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Proof integrity error: ..."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the most important step to understand correctly. The &lt;code&gt;nullifier&lt;/code&gt; parameter is provided by the caller as a public input to &lt;code&gt;verifyAndUse&lt;/code&gt;. But the circuit doesn't trust it; it recomputes the nullifier from the private &lt;code&gt;secret&lt;/code&gt; and &lt;code&gt;context&lt;/code&gt; witnesses and asserts the two match.&lt;/p&gt;

&lt;p&gt;Why does this matter? Without this check, a caller could pass any arbitrary &lt;code&gt;nullifier&lt;/code&gt; value. They could reuse a nullifier from a different context, fabricate one entirely, or replay a proof with a different nullifier to bypass the double-use check. By recomputing the nullifier inside the circuit and binding it to the private secret and context, the contract ensures that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The nullifier is deterministically derived from the prover's actual secret, not fabricated&lt;/li&gt;
&lt;li&gt;The same secret in a different context produces a different nullifier, context scoping works&lt;/li&gt;
&lt;li&gt;Nobody can use someone else's nullifier, the secret is the key&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One design decision worth calling out explicitly: the nullifier is a circuit argument rather than computed purely internally with no public exposure. The circuit could derive &lt;code&gt;computed_nullifier&lt;/code&gt; and insert it directly without ever surfacing it as a parameter, and the proof would be equally valid. But making it an explicit public argument means the TypeScript client must compute the nullifier locally before invoking the contract. This gives the client the ability to query &lt;code&gt;used_nullifiers&lt;/code&gt; on the ledger first and abort early if the nullifier is already recorded, saving the user from generating an expensive ZK proof that would fail on-chain anyway. Proof generation takes meaningful time. Fast UI feedback before that step is worth the minor architectural exposure of making the nullifier a parameter rather than a purely internal value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Merkle path reconstruction&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;h0&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hashLevelNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path_indices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="n"&gt;leaf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="n"&gt;siblings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;h1&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hashLevelNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path_indices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="n"&gt;h0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="n"&gt;siblings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="c1"&gt;// ... 18 more levels ...&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;calculated_root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hashLevelNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path_indices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;h18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;siblings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twenty calls to &lt;code&gt;hashLevelNode&lt;/code&gt;, each feeding the output of the previous as input. This is the full depth-20 Merkle path unrolled manually.&lt;/p&gt;

&lt;p&gt;The reason for manual unrolling rather than a loop has two layers. The deeper one is fundamental to all ZK circuits: they must compile to a fixed-size mathematical constraint system. Any loop must have statically determinable bounds; the circuit size has to be known at key generation time. A loop over a runtime-length vector is impossible in any ZK circuit system, not just Compact.&lt;/p&gt;

&lt;p&gt;The shallower reason is specific to Compact v0.22: the compiler could struggle with variable shadowing, loop state management, and sequential hashing bounds inside a &lt;code&gt;for...in&lt;/code&gt; loop over vectors. Manual unrolling is bulletproof; it produces a mathematically sound constraint system without hitting compiler edge cases. It's verbose, but it compiles cleanly every time. If Compact's loop handling matures in later versions, this could be condensed. For now, twenty explicit lines is the right tradeoff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Root assertion&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;calculated_root&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;merkle_root&lt;/span&gt;&lt;span class="nf"&gt;.read&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"Membership verification failed: ..."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The locally reconstructed root must match what's stored on-chain. This single assertion is the entire membership proof. If the prover provided the wrong secret, the wrong siblings, or the wrong path indices, &lt;code&gt;calculated_root&lt;/code&gt; will differ from &lt;code&gt;merkle_root&lt;/code&gt; at some level of the tree and the proof will fail to generate.&lt;/p&gt;

&lt;p&gt;An invalid proof doesn't just fail at the assert; it fails at the proof generation stage. The proof server cannot produce a valid ZK proof for a circuit that would fail its assertions. This means an invalid membership attempt never even reaches the chain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6: Nullifier recording&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;used_nullifiers&lt;/span&gt;&lt;span class="nf"&gt;.member&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nullifier&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="s"&gt;"Dual-usage detected: ..."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;used_nullifiers&lt;/span&gt;&lt;span class="nf"&gt;.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nullifier&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things happen here. First, the check: if this nullifier is already in &lt;code&gt;used_nullifiers&lt;/code&gt;, the transaction fails. Second, the insert: the nullifier is &lt;code&gt;disclose()&lt;/code&gt;-d into the public set, permanently marking it as used.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;disclose()&lt;/code&gt; on the nullifier is intentional and necessary. The nullifier needs to go on-chain so future proofs can check against it. But notice what it reveals: a 32-byte hash of &lt;code&gt;(domain || secret || context)&lt;/code&gt;. An observer learns that &lt;em&gt;some&lt;/em&gt; secret with &lt;em&gt;some&lt;/em&gt; context was used, not which secret, not which context, not which member.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Admin Pattern: ZK Governance
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;setRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;derived_commitment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;persistentHash&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="nf"&gt;pad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"zk-allowlist:admin:v1"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;getAdminSecret&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;derived_commitment&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;admin_commitment&lt;/span&gt;&lt;span class="nf"&gt;.read&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"Unauthorized: ..."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;merkle_root&lt;/span&gt;&lt;span class="nf"&gt;.write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_root&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same pattern as the escrow's &lt;code&gt;deriveKey&lt;/code&gt; but applied to governance. The admin never stores their identity on-chain. &lt;code&gt;admin_commitment&lt;/code&gt; is just &lt;code&gt;hash(domain || adminSecret)&lt;/code&gt;. Authorization is proven in ZK: the caller demonstrates they know the secret whose commitment matches the on-chain value.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;setup&lt;/code&gt; circuit is a one-time initialization:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;initial_commitment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;admin_commitment&lt;/span&gt;&lt;span class="nf"&gt;.read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;pad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"Setup failed: Administrator already configured"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;admin_commitment&lt;/span&gt;&lt;span class="nf"&gt;.write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;initial_commitment&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The zero-bytes default check works because Compact ledger state variables are zero-initialized by the underlying Midnight ledger if they haven't been written to. Bytes&amp;lt;32&amp;gt; fields start as 32 zero bytes; no explicit initialization needed, no null or undefined state possible. pad(32, "") generates exactly those 32 zero bytes, so the assertion admin_commitment.read() == pad(32, "") is a clean "has this ever been written?" check. Once setup writes a real commitment, it can never be called again.&lt;/p&gt;

&lt;p&gt;Notice that &lt;code&gt;setup&lt;/code&gt; takes &lt;code&gt;initial_commitment&lt;/code&gt; as a circuit argument, not derived from a witness. The admin computes &lt;code&gt;hash(domain || adminSecret)&lt;/code&gt; off-chain and passes it in. The secret itself never appears in the transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Off-Chain Layer: What &lt;code&gt;allowlist-utils.ts&lt;/code&gt; Actually Does
&lt;/h2&gt;

&lt;p&gt;The TypeScript layer in &lt;code&gt;allowlist-utils.ts&lt;/code&gt; handles everything the circuit can't do; stateful data management, Merkle tree construction, and local proof verification before submission.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;generateProof&lt;/code&gt; does three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Finds the leaf index for the given secret in the local tree&lt;/li&gt;
&lt;li&gt;Calls &lt;code&gt;tree.getMerklePath(leafIndex)&lt;/code&gt; to get siblings and path indices&lt;/li&gt;
&lt;li&gt;Verifies the path locally with &lt;code&gt;tree.verifyPath()&lt;/code&gt; before constructing the proof object&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last step is the key one. Local verification before submission is a development-time safeguard; it catches malformed proofs before they hit the proof server, which would generate a valid ZK proof for an invalid membership claim and then have it rejected on-chain. Better to fail fast locally.&lt;/p&gt;

&lt;p&gt;The proof object itself is structured as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;encoded&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;witness&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// private inputs&lt;/span&gt;
    &lt;span class="nx"&gt;publicInputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// current tree root&lt;/span&gt;
        &lt;span class="nx"&gt;nullifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;  &lt;span class="c1"&gt;// hash(secret, context)&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;treeDepth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;generatedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;verified&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;witness&lt;/code&gt; object inside the hex-encoded proof contains &lt;code&gt;secret&lt;/code&gt;, &lt;code&gt;leaf&lt;/code&gt;, &lt;code&gt;leafIndex&lt;/code&gt;, &lt;code&gt;siblings&lt;/code&gt;, and &lt;code&gt;pathIndices&lt;/code&gt;, all the private inputs the proof server needs. In a production deployment these would be consumed by the proof server and discarded; here they're serialized for CLI inspection and local verification.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;verifyProof&lt;/code&gt; mirrors the circuit's logic in TypeScript: recompute the leaf, walk the path, check the root, recompute the nullifier, compare. It's the same five checks the circuit performs, run locally without a proof server. Useful for debugging and for the test suite's 17 forgery attack tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Concatenation Bug the Tests Found
&lt;/h2&gt;

&lt;p&gt;The test suite discovered a real vulnerability worth understanding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hashNullifier("alice", "ctx1") === hashNullifier("alic", "ectx1")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The original implementation hashed &lt;code&gt;domain || secret || context&lt;/code&gt; by simple concatenation. &lt;code&gt;"alice" + "ctx1"&lt;/code&gt; and &lt;code&gt;"alic" + "ectx1"&lt;/code&gt; both produce the string &lt;code&gt;"alicectx1"&lt;/code&gt;, identical inputs, identical hashes, identical nullifiers.&lt;/p&gt;

&lt;p&gt;The fix was a 4-byte big-endian length prefix before the secret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hash(domain || len(secret) || secret || context)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;len("alice") = 5&lt;/code&gt; and &lt;code&gt;len("alic") = 4&lt;/code&gt;, the inputs are structurally distinct. The contract's &lt;code&gt;persistentHash&amp;lt;Vector&amp;lt;3, Bytes&amp;lt;32&amp;gt;&amp;gt;&amp;gt;&lt;/code&gt; call naturally avoids this issue because it operates on fixed-length &lt;code&gt;Bytes&amp;lt;32&amp;gt;&lt;/code&gt; values rather than variable-length strings. But the off-chain TypeScript hashing needed the explicit length prefix fix.&lt;/p&gt;

&lt;p&gt;This is a classic lesson in hash function design: whenever you concatenate variable-length inputs, you need either fixed-length encoding, length prefixes, or a structured type system to prevent collisions across different field boundaries. The Compact circuit is immune because &lt;code&gt;Bytes&amp;lt;32&amp;gt;&lt;/code&gt; is always exactly 32 bytes. The TypeScript layer isn't, and the test suite proved it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Actually on the Chain
&lt;/h2&gt;

&lt;p&gt;Being precise, as always:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Data&lt;/th&gt;
&lt;th&gt;On-chain&lt;/th&gt;
&lt;th&gt;Observer learns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Merkle root&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;The set has this root: nothing about members&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Admin commitment&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Someone controls this contract: not who&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nullifier&lt;/td&gt;
&lt;td&gt;Yes (after use)&lt;/td&gt;
&lt;td&gt;Some secret+context was used: not which&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Leaf hash&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Leaf index&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merkle path&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Member count&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The root encodes the full membership set as a single hash. The nullifier set grows with usage. An observer watching the chain can count how many times the allowlist has been used but cannot learn anything about who used it or who is allowed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The General Pattern
&lt;/h2&gt;

&lt;p&gt;Strip the allowlist semantics and what remains is a reusable primitive for any ZK set membership proof on Midnight:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Off-chain: build a Merkle tree of commitments to private identities&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;leaf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain_leaf&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;leaf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. On-chain: store only the root&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt; &lt;span class="n"&gt;merkle_root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. In the circuit: reconstruct the root from a private path&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 20 levels of hashLevelNode, unrolled&lt;/span&gt;
&lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;calculated_root&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;merkle_root&lt;/span&gt;&lt;span class="nf"&gt;.read&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"Not a member"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Bind usage to a scoped nullifier computed inside the circuit&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;nullifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;persistentHash&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;domain_nullifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;computed_nullifier&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;provided_nullifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Nullifier mismatch"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;used_nullifiers&lt;/span&gt;&lt;span class="nf"&gt;.member&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nullifier&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="s"&gt;"Already used"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;used_nullifiers&lt;/span&gt;&lt;span class="nf"&gt;.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nullifier&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Private airdrops, ZK KYC, anonymous credentials, private DAO membership - all of them are variations on this structure. The tree depth controls the maximum set size. The context string controls the scope of each nullifier. The domain separators ensure hash outputs from different parts of the system never collide.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;The six contracts across this series; bonding curve, state model analysis, escrow, quadratic voting, allowlist, cover the core primitive set for ZK DeFi and governance on Midnight: algorithmic markets, private state management, multi-party coordination, mathematical verification, and membership proofs.&lt;/p&gt;

&lt;p&gt;The natural extension from the allowlist is combining it with the QV contract: members of a private allowlist get quadratic voting power, their membership proven anonymously, their vote weight proven without revealing their token allocation. That's a full private governance primitive and a meaningful next build.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Full source: &lt;a href="https://github.com/tusharpamnani/midnight-allowlist" rel="noopener noreferrer"&gt;github.com/tusharpamnani/midnight-allowlist&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>privacy</category>
      <category>tutorial</category>
      <category>web3</category>
    </item>
    <item>
      <title>How Midnight Verifies tokens Without Computing It</title>
      <dc:creator>Tushar Pamnani</dc:creator>
      <pubDate>Wed, 01 Apr 2026 05:30:00 +0000</pubDate>
      <link>https://dev.to/midnight-aliit/how-midnight-verifies-tokens-without-computing-it-5co6</link>
      <guid>https://dev.to/midnight-aliit/how-midnight-verifies-tokens-without-computing-it-5co6</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 4 of building with Midnight. &lt;a href="https://dev.to/tusharpamnani/building-a-bonding-curve-token-on-midnight-with-real-zk-proofs-2h0i"&gt;Part 1&lt;/a&gt; | &lt;a href="https://dev.to/tusharpamnani/youre-probably-using-export-ledger-wrong-4j1c"&gt;Part 2&lt;/a&gt; | &lt;a href="https://dev.to/tusharpamnani/how-midnight-coordinates-two-party-transfers-3dfh"&gt;Part 3&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Full source: &lt;a href="https://github.com/tusharpamnani/midnight-quadratic-voting" rel="noopener noreferrer"&gt;https://github.com/tusharpamnani/midnight-quadratic-voting&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every article in this series has had one moment where Midnight forces you to think differently. In Part 1 it was the witness pattern; you don't compute inside the circuit, you verify. In Part 3 it was the commitment scheme; you don't share secret values, you prove knowledge of them.&lt;/p&gt;

&lt;p&gt;This article has the same moment, and it's the clearest example yet.&lt;/p&gt;

&lt;p&gt;Quadratic voting requires computing &lt;code&gt;floor(sqrt(tokens))&lt;/code&gt; for every voter. If you're thinking like a Solidity developer, your instinct is to implement a square root function and call it inside the contract. On Midnight, that instinct is wrong, and understanding why leads you directly to one of the most elegant patterns in ZK circuit design.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Quadratic Voting Is and Why It's Hard in ZK
&lt;/h2&gt;

&lt;p&gt;Standard voting gives each participant one vote regardless of their stake. Token-weighted voting gives proportional power to token holders; one token, one vote - which tends toward plutocracy. Quadratic voting is the middle ground: voting power scales as the square root of tokens committed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;weight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The effect: doubling your tokens increases your influence by roughly 41%, not 100%. Whales can still participate but their marginal influence decreases. It's a mechanism design solution to concentration of power.&lt;/p&gt;

&lt;p&gt;The ZK problem is this: &lt;code&gt;sqrt&lt;/code&gt; is not a native operation in arithmetic circuits. ZK proof systems work over finite fields; addition and multiplication are cheap, but square roots require either expensive lookup tables or iterative algorithms that balloon the circuit size. For a practical on-chain implementation, neither is acceptable.&lt;/p&gt;

&lt;p&gt;The solution isn't to compute the square root at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;verifySqrt&lt;/code&gt; Insight
&lt;/h2&gt;

&lt;p&gt;Here's the core circuit from the contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;verifySqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w_sq&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w_sq&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Weight too large: w^2 &amp;gt; tokens"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;two_w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;two_w_plus_1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;two_w&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;w_sq&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;two_w_plus_1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Weight too small: use floor(sqrt(tokens))"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lines of arithmetic. No square root function anywhere. Let's understand exactly what this is doing.&lt;/p&gt;

&lt;p&gt;The key mathematical insight: you don't need to &lt;em&gt;compute&lt;/em&gt; &lt;code&gt;floor(sqrt(tokens))&lt;/code&gt;. You only need to &lt;em&gt;verify&lt;/em&gt; that a claimed value &lt;code&gt;w&lt;/code&gt; satisfies the definition of floor square root.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;w = floor(sqrt(tokens))&lt;/code&gt; if and only if:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;w² ≤ tokens &amp;lt; (w+1)²
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expand &lt;code&gt;(w+1)²&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;w² ≤ tokens &amp;lt; w² + 2w + 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rearrange the right side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="err"&gt;²&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's exactly what the two asserts check. The first assert checks &lt;code&gt;w² ≤ tokens&lt;/code&gt;. The second checks &lt;code&gt;tokens - w² &amp;lt; 2w + 1&lt;/code&gt;. Together they bound &lt;code&gt;w&lt;/code&gt; to be exactly &lt;code&gt;floor(sqrt(tokens))&lt;/code&gt;: no larger, no smaller.&lt;/p&gt;

&lt;p&gt;The caller provides &lt;code&gt;w&lt;/code&gt; and &lt;code&gt;w_sq&lt;/code&gt; as witnesses, computed off-chain in TypeScript where you can use native math:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;w_sq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BigInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nc"&gt;BigInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The circuit never computes a square root. It only verifies two inequalities. Both are just subtraction and comparison, cheap in any arithmetic circuit.&lt;/p&gt;

&lt;p&gt;This is the witness pattern from Part 1 applied to a harder problem. In the bonding curve, &lt;code&gt;calculateCost&lt;/code&gt; computed the integral off-chain and &lt;code&gt;verifiedHalfProduct&lt;/code&gt; verified it with a multiplication check. Here, &lt;code&gt;getSqrtWeight&lt;/code&gt; computes the square root off-chain and &lt;code&gt;verifySqrt&lt;/code&gt; verifies it with two inequality checks. The structure is identical; the mathematical trick is different.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Full Contract, Annotated
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;pragma&lt;/span&gt; &lt;span class="n"&gt;language_version&lt;/span&gt; &lt;span class="mf"&gt;0.22&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CompactStandardLibrary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Public ledger state&lt;/span&gt;
&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt; &lt;span class="n"&gt;committed_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt; &lt;span class="n"&gt;has_voted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt; &lt;span class="n"&gt;total_votes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Witnesses — computed off-chain, verified in-circuit&lt;/span&gt;
&lt;span class="n"&gt;witness&lt;/span&gt; &lt;span class="nf"&gt;getVoterId&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;witness&lt;/span&gt; &lt;span class="nf"&gt;getCommittedTokens&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;witness&lt;/span&gt; &lt;span class="nf"&gt;getSqrtWeight&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;witness&lt;/span&gt; &lt;span class="nf"&gt;getWSquared&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four witnesses. Notice &lt;code&gt;getWSquared()&lt;/code&gt; exists separately from &lt;code&gt;getSqrtWeight()&lt;/code&gt;, rather than computing &lt;code&gt;w²&lt;/code&gt; inside the circuit (a multiplication), the caller provides it pre-computed and the circuit verifies the relationship. This is the same philosophy as the bonding curve: push arithmetic off-chain, verify the result cheaply.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;computeNullifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;voter_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;persistentHash&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;voter_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The nullifier is &lt;code&gt;hash(voter_id)&lt;/code&gt;. One voter ID maps to exactly one nullifier, deterministically, forever. This prevents double voting: once a nullifier is in &lt;code&gt;has_voted&lt;/code&gt;, that voter ID can never vote again regardless of what proof they generate.&lt;/p&gt;

&lt;p&gt;The comment in the source is worth quoting directly: &lt;em&gt;"Current compiler signature restricts &lt;code&gt;persistentHash&lt;/code&gt; to a single &lt;code&gt;Bytes&amp;lt;32&amp;gt;&lt;/code&gt; argument. For now, we hash the voter_id directly."&lt;/em&gt; This is an honest acknowledgment of a current Compact compiler constraint. The README flags the consequence: same voter_id across different contracts produces the same nullifier, creating cross-contract linkability. The fix: &lt;code&gt;hash(voter_id, contract_id)&lt;/code&gt;, is straightforward once the compiler supports multi-argument hashing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;voter_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;committed_tokens&lt;/span&gt;&lt;span class="nf"&gt;.member&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;voter_id&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="s"&gt;"Already committed"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;committed_tokens&lt;/span&gt;&lt;span class="nf"&gt;.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;voter_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the circuit where we need to be honest about the current privacy model.&lt;/p&gt;

&lt;p&gt;Both &lt;code&gt;voter_id&lt;/code&gt; and &lt;code&gt;tokens&lt;/code&gt; are &lt;code&gt;disclose()&lt;/code&gt;-d into the public ledger. The README lists these as private, but the compiler tells the true story: any value passed through &lt;code&gt;disclose()&lt;/code&gt; into an &lt;code&gt;export ledger&lt;/code&gt; field is fully visible on-chain. Right now, an observer can see which voter IDs have committed and exactly how many tokens each holds.&lt;/p&gt;

&lt;p&gt;This is the same situation as the bonding curve balances from Part 1: public ledger, not private state. The genuinely private version would use &lt;code&gt;persistentCommit&lt;/code&gt; to store a commitment to &lt;code&gt;(voter_id, tokens, nonce)&lt;/code&gt; on-chain, with the actual values held in local private state. The &lt;code&gt;vote&lt;/code&gt; circuit would then verify the commitment rather than checking the plaintext map. That's the architecture the README's privacy model describes and the next iteration of this contract should implement.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;vote&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;voter_id&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getVoterId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getCommittedTokens&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;sqrt_weight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getSqrtWeight&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;w_sq&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getWSquared&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;computed_nullifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computeNullifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;voter_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Verify the voter has a commitment&lt;/span&gt;
    &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;committed_tokens&lt;/span&gt;&lt;span class="nf"&gt;.member&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;voter_id&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="s"&gt;"No commitment found"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Verify the committed amount matches the witness&lt;/span&gt;
    &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;committed_tokens&lt;/span&gt;&lt;span class="nf"&gt;.lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;voter_id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"Token mismatch"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Verify the quadratic constraint in ZK&lt;/span&gt;
    &lt;span class="nf"&gt;verifySqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sqrt_weight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w_sq&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Prevent double voting&lt;/span&gt;
    &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;has_voted&lt;/span&gt;&lt;span class="nf"&gt;.member&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;computed_nullifier&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="s"&gt;"Already voted"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Record the vote&lt;/span&gt;
    &lt;span class="n"&gt;has_voted&lt;/span&gt;&lt;span class="nf"&gt;.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;computed_nullifier&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;total_votes&lt;/span&gt;&lt;span class="nf"&gt;.increment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sqrt_weight&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Walk through the sequence of checks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Membership check&lt;/strong&gt;: the voter committed tokens in a prior transaction. Without this, anyone could vote with an arbitrary weight without having committed anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token match&lt;/strong&gt;: the &lt;code&gt;tokens&lt;/code&gt; witness matches what's actually stored in the commitment map. This closes a subtle attack: without this check, a voter could commit 100 tokens then claim 10,000 tokens at vote time, pass the &lt;code&gt;verifySqrt&lt;/code&gt; check with a legitimate &lt;code&gt;w&lt;/code&gt; for 10,000, and vote with an inflated weight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quadratic constraint&lt;/strong&gt;: &lt;code&gt;verifySqrt&lt;/code&gt; runs. The circuit verifies &lt;code&gt;w = floor(sqrt(tokens))&lt;/code&gt; without computing a square root.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Double vote check&lt;/strong&gt;: the nullifier hasn't been used. Combined with the deterministic nullifier derivation, this guarantees exactly one vote per committed voter ID.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State updates&lt;/strong&gt;: nullifier recorded, total votes incremented by &lt;code&gt;sqrt_weight&lt;/code&gt;. The global tally reflects the sum of all voters' square-root-weighted contributions.&lt;/p&gt;

&lt;p&gt;The ordering of these checks matters. Membership before token match (can't verify a value that doesn't exist). Token match before &lt;code&gt;verifySqrt&lt;/code&gt; (the constraint is meaningless if the token count can be faked). Double vote check before state update (record only after all validation passes).&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Actually On-Chain
&lt;/h2&gt;

&lt;p&gt;Being precise, same as the previous articles:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Data&lt;/th&gt;
&lt;th&gt;On-chain?&lt;/th&gt;
&lt;th&gt;Visible to observer?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Voter ID&lt;/td&gt;
&lt;td&gt;Yes (&lt;code&gt;committed_tokens&lt;/code&gt; key)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token amount&lt;/td&gt;
&lt;td&gt;Yes (&lt;code&gt;committed_tokens&lt;/code&gt; value)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Square root weight&lt;/td&gt;
&lt;td&gt;Yes (via &lt;code&gt;total_votes&lt;/code&gt; increment)&lt;/td&gt;
&lt;td&gt;Partially — total only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nullifier&lt;/td&gt;
&lt;td&gt;Yes (&lt;code&gt;has_voted&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Individual vote weight&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Whether a specific voter voted&lt;/td&gt;
&lt;td&gt;Derivable from &lt;code&gt;has_voted&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Yes, via nullifier&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The chain sees voter IDs and token amounts in the current implementation. The &lt;code&gt;total_votes&lt;/code&gt; counter accumulates the global weighted tally but individual vote weights aren't stored separately: an observer can see the total but not decompose whose weight contributed what.&lt;/p&gt;

&lt;p&gt;The nullifier being on-chain is intentional and necessary: it's the double-vote prevention mechanism. What it leaks is that a given voter ID has voted, derivable by anyone who knows the voter ID and can compute &lt;code&gt;hash(voter_id)&lt;/code&gt;. In the domain-separated future version, this becomes &lt;code&gt;hash(voter_id, contract_id)&lt;/code&gt;, still derivable by someone who knows the voter ID, but no longer linkable across contracts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mental Model This Series Has Been Building
&lt;/h2&gt;

&lt;p&gt;Step back and look at what the four contracts in this series have taught:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bonding curve&lt;/strong&gt;: the witness pattern. Expensive arithmetic off-chain, cheap verification in-circuit. The circuit doesn't trust the witness; it constrains it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;export ledger&lt;/code&gt; vs private state&lt;/strong&gt;: the disclosure model. &lt;code&gt;disclose()&lt;/code&gt; is a deliberate act, not a default. The compiler enforces it. Privacy is opt-out at the language level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Escrow&lt;/strong&gt;: multi-party coordination. Two private states, one proof. Commitment schemes bind parties to terms without revealing them. State machines enforce ordering cryptographically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quadratic voting&lt;/strong&gt;: verification over computation. The circuit doesn't compute &lt;code&gt;sqrt&lt;/code&gt;. It verifies that someone else's claimed &lt;code&gt;sqrt&lt;/code&gt; satisfies the mathematical definition. The same pattern applies to any expensive function: compute off-chain, verify with cheaper constraints in-circuit.&lt;/p&gt;

&lt;p&gt;That last principle is the most general. Anywhere you find yourself wanting to compute something expensive inside a ZK circuit; a square root, a logarithm, a hash of a large input - ask instead: what are the cheap constraints that a valid result must satisfy? Verify those instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;The natural extension of this contract, private token commitments using &lt;code&gt;persistentCommit&lt;/code&gt; instead of plaintext &lt;code&gt;export ledger&lt;/code&gt; maps, is exactly the bridge between this article and Part 2's commitment pattern. If you want a hands-on extension problem: refactor &lt;code&gt;commit&lt;/code&gt; to store &lt;code&gt;persistentCommit(voter_id, tokens, nonce)&lt;/code&gt; on-chain, move the actual values to private state, and update &lt;code&gt;vote&lt;/code&gt; to verify the commitment before running &lt;code&gt;verifySqrt&lt;/code&gt;. The quadratic constraint circuit stays identical, only the token retrieval and verification changes.&lt;/p&gt;

&lt;p&gt;The allowlist contract, which uses Merkle trees, domain-separated nullifiers, and a proper off-chain membership proof system, is the next full article in this series. It takes the nullifier pattern introduced here and builds a complete private membership system around it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Full source for this contract and the rest of the series: &lt;a href="https://github.com/tusharpamnani/midnight-quadratic-voting" rel="noopener noreferrer"&gt;github.com/tusharpamnani/midnight-quadratic-voting&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>midnight</category>
      <category>zeroknowledge</category>
      <category>web3</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>How Midnight Coordinates Two-Party Transfers</title>
      <dc:creator>Tushar Pamnani</dc:creator>
      <pubDate>Sat, 28 Mar 2026 21:33:34 +0000</pubDate>
      <link>https://dev.to/midnight-aliit/how-midnight-coordinates-two-party-transfers-3dfh</link>
      <guid>https://dev.to/midnight-aliit/how-midnight-coordinates-two-party-transfers-3dfh</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 3 of building with Midnight. &lt;a href="https://dev.to/tusharpamnani/building-a-bonding-curve-token-on-midnight-with-real-zk-proofs-2h0i"&gt;Part 1&lt;/a&gt; | &lt;a href="https://dev.to/tusharpamnani/youre-probably-using-export-ledger-wrong-4j1c"&gt;Part 2&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Part 2 ended with a provocation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"The next level is understanding how private state interacts with transfers between users, which requires both parties to update their local private state atomically, coordinated by a proof that neither can fake."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's not theoretical anymore. The &lt;a href="https://github.com/sevryn-labs/midnight-escrow" rel="noopener noreferrer"&gt;midnight-escrow&lt;/a&gt; contract is a working implementation of exactly this problem, and it's a much richer teaching tool than any pseudocode example.&lt;/p&gt;

&lt;p&gt;This article is a circuit-level autopsy of that contract. We're going to read the actual Compact code, understand &lt;em&gt;why&lt;/em&gt; each line exists from a cryptographic coordination perspective, and extract the general pattern that applies to any private multi-party transfer on Midnight.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Coordination Problem, Stated Precisely
&lt;/h2&gt;

&lt;p&gt;Before touching code, let's be precise about what makes multi-party transfers hard on Midnight.&lt;/p&gt;

&lt;p&gt;In Solidity you write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;balances[alice] -= amount;
balances[bob]   += amount;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both writes happen in one EVM transaction. Atomicity is free because the EVM's state is a single shared namespace: one machine, one write.&lt;/p&gt;

&lt;p&gt;In Midnight, the state model is split:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Public ledger (P)  →  every node on the network, plaintext
Private state (S)  →  user's local machine, never on-chain
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alice's private balance lives on Alice's machine. Bob's lives on Bob's. There is no shared namespace for private values. And a ZK proof can only attest to a state transition for &lt;em&gt;one&lt;/em&gt; private state at a time.&lt;/p&gt;

&lt;p&gt;So the question becomes: &lt;strong&gt;how do you make two separate private state updates, on two different machines, behave atomically?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The escrow contract answers this with three interlocking mechanisms:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;commitment scheme&lt;/strong&gt; that binds the transfer terms cryptographically&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;derived identity system&lt;/strong&gt; that ties circuit authorization to private keys without exposing them&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;state machine&lt;/strong&gt; that enforces ordering and makes intermediate exploitation impossible&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's walk through each.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Ledger: What's Actually Public
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export ledger buyer: Bytes&amp;lt;32&amp;gt;;
export ledger seller: Bytes&amp;lt;32&amp;gt;;
export ledger termsCommitment: Bytes&amp;lt;32&amp;gt;;
export ledger state: EscrowState;
export ledger round: Counter;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five fields. That's the entire public surface of this contract.&lt;/p&gt;

&lt;p&gt;Notice what's &lt;em&gt;not&lt;/em&gt; here: no balance, no amount, no secret, no nonce. An observer watching the chain sees two 32-byte identifiers, a 32-byte blob, a state enum, and a counter.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;buyer&lt;/code&gt; and &lt;code&gt;seller&lt;/code&gt; are not wallet addresses: they are &lt;em&gt;derived keys&lt;/em&gt;. We'll get to the derivation in a moment, but this is one of the most important design decisions in the contract.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;termsCommitment&lt;/code&gt; is the cryptographic core. It's a commitment to the transfer terms: the amount and the release secret. The buyer posts it at creation; the seller must open it at release. Everything else in the protocol flows from this one field.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Witnesses: The Private Side
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;EscrowPrivateState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// 32 bytes — identity key&lt;/span&gt;
  &lt;span class="nl"&gt;releaseSecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 32 bytes — pre-image of the commitment&lt;/span&gt;
  &lt;span class="nl"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="c1"&gt;// 32 bytes — commitment randomness&lt;/span&gt;
  &lt;span class="nl"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// escrow value&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the corresponding Compact witness declarations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;witness secretKey(): Bytes&amp;lt;32&amp;gt;;
witness releaseSecret(): Bytes&amp;lt;32&amp;gt;;
witness nonce(): Bytes&amp;lt;32&amp;gt;;
witness escrowAmount(): Uint&amp;lt;64&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These four values never touch the chain. They live in &lt;code&gt;EscrowPrivateState&lt;/code&gt;, which the wallet SDK manages locally. At proof generation time, the witness functions feed them into the circuit, the Compact compiler sees them as private inputs to the ZK proof.&lt;/p&gt;

&lt;p&gt;The witness implementation is deliberately simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;witnesses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WitnessContext&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Ledger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;EscrowPrivateState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;EscrowPrivateState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;privateState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ... same pattern for releaseSecret, nonce, escrowAmount&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each witness function returns a tuple of &lt;code&gt;[newPrivateState, witnessValue]&lt;/code&gt;. In this contract, private state is read-only; the returned state is always identical to the input. This makes sense: both parties hold fixed secrets for the entire duration of the escrow. Neither needs to update their local state between circuit calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identity Without Exposing Keys: &lt;code&gt;deriveKey&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;circuit deriveKey(sk: Bytes&amp;lt;32&amp;gt;): Bytes&amp;lt;32&amp;gt; {
  return persistentHash&amp;lt;Vector&amp;lt;2, Bytes&amp;lt;32&amp;gt;&amp;gt;&amp;gt;(
    [pad(32, "midnight:escrow:key"), sk]
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This circuit is one of the most important pieces in the contract and consistently the least explained.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;buyer&lt;/code&gt; and &lt;code&gt;seller&lt;/code&gt; ledger fields hold &lt;em&gt;derived public keys&lt;/em&gt;, not raw addresses or standard ECDSA public keys. Concretely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;derived_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;persistentHash&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;midnight:escrow:key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why not store a standard public key and verify a signature? You could, but you'd need signature verification logic inside the circuit, which adds constraints. The derived key approach is more elegant: &lt;code&gt;secretKey&lt;/code&gt; is a private witness, &lt;code&gt;deriveKey(sk)&lt;/code&gt; computes a public identifier from it &lt;em&gt;inside the proof&lt;/em&gt;, and the circuit asserts the computed value matches what's on the ledger. The ZK proof itself is the authorization, no separate signature scheme needed.&lt;/p&gt;

&lt;p&gt;The domain separator &lt;code&gt;"midnight:escrow:key"&lt;/code&gt; (padded to 32 bytes) is critical. Without it, the same &lt;code&gt;secretKey&lt;/code&gt; used in two different Midnight contracts would produce the same derived key on-chain, creating a linkage across the ecosystem. The separator namespace-isolates key derivation to this contract.&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;createEscrow&lt;/code&gt; runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const sk = secretKey();   // private witness — never leaves the proof
const pk = deriveKey(sk); // computed inside the circuit
buyer = disclose(pk);     // only the derived key goes on-chain
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The chain learns &lt;code&gt;pk&lt;/code&gt;. It learns nothing about &lt;code&gt;sk&lt;/code&gt;. But anyone who later calls a circuit with the same &lt;code&gt;sk&lt;/code&gt; will produce the same &lt;code&gt;pk&lt;/code&gt;, proving identity without revealing the key.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Commitment Scheme: Binding Terms Without Revealing Them
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export circuit createEscrow(
  sellerPk: Bytes&amp;lt;32&amp;gt;,
  amount: Uint&amp;lt;64&amp;gt;
): [] {
  assert(state == EscrowState.EMPTY, "Escrow already exists");

  const sk     = secretKey();
  const pk     = deriveKey(sk);
  const n      = nonce();
  const secret = releaseSecret();

  const releaseHash = persistentHash&amp;lt;Bytes&amp;lt;32&amp;gt;&amp;gt;(secret);

  const termsCommit = persistentCommit&amp;lt;Vector&amp;lt;2, Bytes&amp;lt;32&amp;gt;&amp;gt;&amp;gt;(
    [amount as Bytes&amp;lt;32&amp;gt;, releaseHash],
    n
  );

  buyer           = disclose(pk);
  seller          = disclose(sellerPk);
  termsCommitment = disclose(termsCommit);
  state           = EscrowState.FUNDED;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's trace what gets committed and why each decision was made.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: hash the release secret first&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const releaseHash = persistentHash&amp;lt;Bytes&amp;lt;32&amp;gt;&amp;gt;(secret);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The commitment will contain &lt;code&gt;hash(secret)&lt;/code&gt;, not &lt;code&gt;secret&lt;/code&gt; itself. This is a double layer of hiding: even if someone could theoretically invert &lt;code&gt;persistentCommit&lt;/code&gt;, they'd only recover the hash, not the original pre-image.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: commit to &lt;code&gt;[amount, releaseHash]&lt;/code&gt; with the nonce&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const termsCommit = persistentCommit&amp;lt;Vector&amp;lt;2, Bytes&amp;lt;32&amp;gt;&amp;gt;&amp;gt;(
  [amount as Bytes&amp;lt;32&amp;gt;, releaseHash],
  n
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;persistentCommit&lt;/code&gt; is binding and hiding. The nonce &lt;code&gt;n&lt;/code&gt; ensures two escrows with the same amount and the same secret produce &lt;em&gt;different&lt;/em&gt; &lt;code&gt;termsCommitment&lt;/code&gt; values on-chain, preventing correlation. Reuse the nonce and two identical commitments would appear on-chain, allowing an observer to link them.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;persistent&lt;/code&gt; prefix matters. The Compact docs distinguish: &lt;code&gt;transientCommit&lt;/code&gt; outputs are suitable only for within-proof use; &lt;code&gt;persistentCommit&lt;/code&gt; outputs are designed for storage in ledger state. Use &lt;code&gt;transientCommit&lt;/code&gt; here and the commitment can't be reliably verified later. &lt;code&gt;persistentCommit&lt;/code&gt; is the correct choice for anything that goes into &lt;code&gt;export ledger&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: disclose only the derived values&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;buyer           = disclose(pk);
seller          = disclose(sellerPk);
termsCommitment = disclose(termsCommit);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three &lt;code&gt;disclose()&lt;/code&gt; calls. As established in Part 2, each is a deliberate declaration to the compiler: "I know this witness-derived value is going public." The compiler rejects this circuit without them. And each disclosure is intentional; the buyer's identity, the seller's identity, and the commitment all &lt;em&gt;need&lt;/em&gt; to be on-chain for the protocol to function.&lt;/p&gt;

&lt;p&gt;What's not disclosed: &lt;code&gt;secretKey&lt;/code&gt;, &lt;code&gt;releaseSecret&lt;/code&gt;, &lt;code&gt;nonce&lt;/code&gt;, &lt;code&gt;amount&lt;/code&gt;. The chain sees the commitment but cannot reverse it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Verification: How the Seller Proves Knowledge of the Secret
&lt;/h2&gt;

&lt;p&gt;This is the cryptographic heart of the contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export circuit release(): [] {
  assert(state == EscrowState.FUNDED, "Invalid state");

  const sk = secretKey();
  const pk = deriveKey(sk);
  assert(pk == seller, "Only seller can release");

  const secret = releaseSecret();
  const n      = nonce();
  const amt    = escrowAmount();

  const secretHash = persistentHash&amp;lt;Bytes&amp;lt;32&amp;gt;&amp;gt;(secret);
  const recomputed = persistentCommit&amp;lt;Vector&amp;lt;2, Bytes&amp;lt;32&amp;gt;&amp;gt;&amp;gt;(
    [amt as Bytes&amp;lt;32&amp;gt;, secretHash],
    n
  );

  assert(
    recomputed == termsCommitment,
    "Invalid release proof"
  );

  state = EscrowState.RELEASED;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two assertions. Two completely different things being verified.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assertion 1: Identity&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;assert(pk == seller, "Only seller can release");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The seller proves they hold the &lt;code&gt;secretKey&lt;/code&gt; that produced the &lt;code&gt;seller&lt;/code&gt; field at creation. Without the correct &lt;code&gt;sk&lt;/code&gt;, &lt;code&gt;deriveKey(sk)&lt;/code&gt; produces the wrong hash and the assertion fails. No one else can generate a valid proof for this circuit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assertion 2: Knowledge of the secret&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;assert(recomputed == termsCommitment, "Invalid release proof");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The seller recomputes the commitment from scratch using their private &lt;code&gt;releaseSecret&lt;/code&gt;, &lt;code&gt;nonce&lt;/code&gt;, and &lt;code&gt;escrowAmount&lt;/code&gt;. If any of these don't match what the buyer committed to in &lt;code&gt;createEscrow&lt;/code&gt;, &lt;code&gt;recomputed != termsCommitment&lt;/code&gt; and the proof fails.&lt;/p&gt;

&lt;p&gt;This is the binding. The buyer chose &lt;code&gt;(amount, releaseSecret, nonce)&lt;/code&gt; and committed to them. The seller must produce the exact same triple. The only way a seller could have the correct &lt;code&gt;(releaseSecret, nonce)&lt;/code&gt; is if the buyer shared them, which is the off-chain handshake step in the protocol. The ZK proof doesn't replace that handshake. It &lt;em&gt;verifies&lt;/em&gt; that the handshake happened correctly, without the chain ever seeing what was exchanged.&lt;/p&gt;

&lt;p&gt;None of these values appear in the public output of the proof. The chain verifies the ZK proof, updates &lt;code&gt;state&lt;/code&gt; to &lt;code&gt;RELEASED&lt;/code&gt;, and that's it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Full Protocol Flow
&lt;/h2&gt;

&lt;p&gt;Here's how the three circuits sequence for a complete transfer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bob (Buyer)                          Chain                        Alice (Seller)
     │                                 │                                │
     │── createEscrow(alicePk, amt) ──▶│                                │
     │   Proof asserts:                │  Ledger after:                 │
     │   - deriveKey(bobSk) == buyer   │  buyer    = H(bobSk)           │
     │   - commit([amt,H(secret)],n)   │  seller   = alicePk            │
     │                                 │  terms    = commit(...)        │
     │                                 │  state    = FUNDED             │
     │                                 │                                │
     │◀── share(nonce, secret) off-chain ─────────────────────────────▶│
     │                                 │                                │
     │                                 │◀── acceptEscrow() ─────────────│
     │                                 │    Proof: deriveKey(aliceSk)   │
     │                                 │    == seller                   │
     │                                 │                                │
     │                                 │◀── release() ──────────────────│
     │                                 │    Proof:                      │
     │                                 │    - deriveKey(aliceSk)==seller│
     │                                 │    - recomputed==termsCommit   │
     │                                 │  state = RELEASED              │
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The off-chain &lt;code&gt;share(nonce, secret)&lt;/code&gt; step is the only coordination that happens outside the chain. Bob hands Alice these two values; the CLI prints them, they copy-paste. The chain never sees this handoff. The ZK proof in &lt;code&gt;release()&lt;/code&gt; is what cryptographically verifies Alice received the right values and used them correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;acceptEscrow&lt;/code&gt;: The Lightweight Identity Check
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export circuit acceptEscrow(): [] {
  assert(state == EscrowState.FUNDED, "Escrow not funded");
  const sk = secretKey();
  const pk = deriveKey(sk);
  assert(pk == seller, "Only seller can accept");
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This circuit does one thing: proves Alice is the intended seller. No state change beyond the proof itself.&lt;/p&gt;

&lt;p&gt;Why does this exist separately from &lt;code&gt;release&lt;/code&gt;? Two reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confirmation before commitment.&lt;/strong&gt; Before Alice calls &lt;code&gt;release&lt;/code&gt;, she verifies she's looking at the right escrow; the one Bob created for her specifically. &lt;code&gt;acceptEscrow&lt;/code&gt; lets her confirm &lt;code&gt;seller == deriveKey(aliceSk)&lt;/code&gt; before she's asked to provide &lt;code&gt;releaseSecret&lt;/code&gt; and &lt;code&gt;nonce&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Extension point.&lt;/strong&gt; In a richer system, a swap scenario, for example - &lt;code&gt;acceptEscrow&lt;/code&gt; might trigger a state change (&lt;code&gt;FUNDED → ACCEPTED&lt;/code&gt;) that signals Bob to release his side of an asset exchange. Keeping it as a separate circuit preserves that hook without changing the release logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;refund&lt;/code&gt;: The Escape Hatch
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export circuit refund(): [] {
  assert(state == EscrowState.FUNDED, "Invalid state");
  const sk = secretKey();
  const pk = deriveKey(sk);
  assert(pk == buyer, "Only buyer can refund");
  state = EscrowState.REFUNDED;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No timeout logic. No round check. Bob can call &lt;code&gt;refund&lt;/code&gt; at any time while the escrow is &lt;code&gt;FUNDED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is a deliberate design choice for the current implementation, and one the repo explicitly flags as a future improvement. In production you'd gate &lt;code&gt;refund&lt;/code&gt; on a round counter so Alice has a guaranteed window to call &lt;code&gt;release&lt;/code&gt;. The &lt;code&gt;round: Counter&lt;/code&gt; ledger field exists precisely for this; it just isn't wired to &lt;code&gt;refund&lt;/code&gt; yet.&lt;/p&gt;

&lt;p&gt;But even without the timeout, the liveness guarantee is clear: if Alice disappears, Bob isn't stuck. The state machine ensures &lt;code&gt;refund&lt;/code&gt; and &lt;code&gt;release&lt;/code&gt; are mutually exclusive, once either fires, &lt;code&gt;state&lt;/code&gt; is no longer &lt;code&gt;FUNDED&lt;/code&gt; and the other becomes impossible at the circuit level.&lt;/p&gt;

&lt;h2&gt;
  
  
  State Machine: Why Ordering Is a Security Property
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;enum EscrowState {
  EMPTY,
  FUNDED,
  RELEASED,
  REFUNDED
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every circuit opens with an &lt;code&gt;assert&lt;/code&gt; on &lt;code&gt;state&lt;/code&gt;. This isn't defensive programming, it's the atomicity guarantee.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EMPTY  ──createEscrow──▶  FUNDED  ──release──▶  RELEASED
                                  └──refund───▶  REFUNDED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once &lt;code&gt;state = EscrowState.RELEASED&lt;/code&gt;, the escrow is frozen. &lt;code&gt;release&lt;/code&gt; requires &lt;code&gt;FUNDED&lt;/code&gt;. &lt;code&gt;refund&lt;/code&gt; requires &lt;code&gt;FUNDED&lt;/code&gt;. &lt;code&gt;createEscrow&lt;/code&gt; requires &lt;code&gt;EMPTY&lt;/code&gt;. There is no path back, no double-release, no re-creation over an existing escrow.&lt;/p&gt;

&lt;p&gt;Consider what happens without these assertions: the buyer could call &lt;code&gt;createEscrow&lt;/code&gt; twice, overwriting &lt;code&gt;termsCommitment&lt;/code&gt; with a different nonce mid-flight. The seller could call &lt;code&gt;release&lt;/code&gt; twice. The state machine makes all of these impossible; not as application-layer logic, but as ZK-verified constraints inside the proof. The chain will not accept a proof that fails a circuit assertion.&lt;/p&gt;

&lt;p&gt;This is the Midnight equivalent of reentrancy protection, enforced by the proof system, not by a mutex.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Privacy Accounting: What Leaks and What Doesn't
&lt;/h2&gt;

&lt;p&gt;Being precise about what an on-chain observer actually learns:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Information&lt;/th&gt;
&lt;th&gt;Buyer knows&lt;/th&gt;
&lt;th&gt;Seller knows&lt;/th&gt;
&lt;th&gt;Chain sees&lt;/th&gt;
&lt;th&gt;Observer learns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Transfer amount&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓ (after sharing)&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Release secret&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓ (after sharing)&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nonce&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓ (after sharing)&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Buyer identity&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;code&gt;H(buyerSk)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pseudonym only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seller identity&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sellerPk&lt;/code&gt; as passed&lt;/td&gt;
&lt;td&gt;Pseudonym only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;That a transfer occurred&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transfer direction (who→who)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;buyer&lt;/code&gt; and &lt;code&gt;seller&lt;/code&gt; are on-chain; direction is visible. This is a deliberate tradeoff: escrow semantically requires knowing who holds which role. A symmetric private swap would need a different design where both parties are represented symmetrically.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;termsCommitment&lt;/code&gt; field is worth a specific note: it's 32 bytes on-chain, visible to everyone. But it reveals nothing about the underlying values. Two escrows with identical amounts and secrets but different nonces produce completely different commitments, the nonce is what ensures this, and it never leaves the buyer's private state.&lt;/p&gt;

&lt;h2&gt;
  
  
  The General Pattern: What Escrow Teaches Us
&lt;/h2&gt;

&lt;p&gt;Strip the escrow semantics away and what remains is a reusable coordination primitive for any private multi-party transfer on Midnight:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Commit to transfer terms at initiation&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;termsCommitment = disclose(persistentCommit([value, hash(secret)], nonce));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Bind identities to private keys via derivation&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const pk = deriveKey(secretKey());
assert(pk == onChainIdentity, "Auth failed");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Verify the commitment at completion&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const recomputed = persistentCommit([value, hash(secret)], nonce);
assert(recomputed == termsCommitment, "Proof failed");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Enforce ordering via state machine&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;assert(state == ExpectedState, "Wrong state");
// ... logic ...
state = NextState;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Private DEX swaps, confidential lending, sealed-bid auctions; all of them are variations on this structure. They need to bind two parties to agreed terms, verify those terms without revealing them, and enforce who moves first. The escrow contract is the cleanest working example of how Midnight's primitives compose to solve it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;The repo explicitly lists its open extensions: timeout-based refunds, multi-party agreements, private dispute resolution. Each is a direct extension of the patterns here. The commitment scheme generalises to more parties by including more values in the committed vector. The state machine grows more states. The identity checks stay identical, &lt;code&gt;deriveKey(secretKey())&lt;/code&gt; works regardless of how many participants are involved.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Part 1: &lt;a href="https://dev.to/tusharpamnani/building-a-bonding-curve-token-on-midnight-with-real-zk-proofs-2h0i"&gt;Building a Bonding Curve Token on Midnight&lt;/a&gt;&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Part 2: &lt;a href="https://dev.to/tusharpamnani/youre-probably-using-export-ledger-wrong-4j1c"&gt;You're Probably Using export ledger Wrong&lt;/a&gt;&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Full source: &lt;a href="https://github.com/sevryn-labs/midnight-escrow" rel="noopener noreferrer"&gt;github.com/sevryn-labs/midnight-escrow&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>midnight</category>
      <category>zeroknowledge</category>
      <category>web3</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>You're Probably Using export ledger Wrong</title>
      <dc:creator>Tushar Pamnani</dc:creator>
      <pubDate>Fri, 27 Mar 2026 17:51:16 +0000</pubDate>
      <link>https://dev.to/midnight-aliit/youre-probably-using-export-ledger-wrong-4j1c</link>
      <guid>https://dev.to/midnight-aliit/youre-probably-using-export-ledger-wrong-4j1c</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 2 of building with Midnight. &lt;a href="https://dev.to/tusharpamnani/building-a-bonding-curve-token-on-midnight-with-real-zk-proofs-2h0i"&gt;Part 1 here.&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In the first article we built a bonding curve on Midnight and were honest about something most ecosystem articles aren't: the token balances in that contract are fully public. Not because Midnight can't do better; it can, but because we used &lt;code&gt;export ledger&lt;/code&gt; for everything and wrapped every balance update in &lt;code&gt;disclose()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's the default pattern developers reach for when coming from Solidity, and it's the wrong instinct on Midnight.&lt;/p&gt;

&lt;p&gt;This article is specifically about that decision: what &lt;code&gt;export ledger&lt;/code&gt; actually means at the protocol level, what private state actually means, what &lt;code&gt;disclose()&lt;/code&gt; is doing as a compile-time mechanism, and when you should use each. By the end you'll have a clear mental model and a concrete refactor path for the bonding curve balances.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model you need to drop first
&lt;/h2&gt;

&lt;p&gt;Coming from Solidity, your mental model of contract state is a single flat namespace. Everything in storage is public. Privacy is something you bolt on externally; encryption, commit-reveal, ZK gadgets. The language doesn't have an opinion about it.&lt;/p&gt;

&lt;p&gt;Midnight inverts this completely. In Compact, private information should be disclosed only as necessary, and the language requires disclosure to be explicitly declared. This makes privacy the default and disclosure an explicit exception, reducing the risk of accidental disclosure.&lt;/p&gt;

&lt;p&gt;That sentence is doing a lot. Privacy is the &lt;em&gt;default&lt;/em&gt;. Disclosure is an &lt;em&gt;exception&lt;/em&gt; you have to actively declare. The compiler enforces this; it will reject your program if you try to move witness data to the public ledger without explicitly saying so.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The practical implication: every piece of state in a Midnight contract starts in one of two worlds, and moving between them requires a deliberate act.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  World 1: &lt;code&gt;export ledger&lt;/code&gt;: the public world
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ledger&lt;/code&gt; declarations represent the local state of the smart contract, the state that is kept on-chain. The values in the ledger are visible and public.&lt;/p&gt;

&lt;p&gt;When you write this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt; &lt;span class="n"&gt;totalSupply&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt; &lt;span class="n"&gt;balances&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;both fields are synchronized across every node on the network. Any observer can read them. They are as public as a Solidity mapping, no caveats.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;export&lt;/code&gt; keyword does two additional things beyond making the field public: it makes the field readable from the TypeScript/JavaScript side of your dApp, and it makes it part of the deployed contract's on-chain interface. You need &lt;code&gt;export&lt;/code&gt; on any ledger field your frontend will read.&lt;/p&gt;

&lt;p&gt;This is the right choice for state that the market &lt;em&gt;needs&lt;/em&gt; to see. In the bonding curve, &lt;code&gt;totalSupply&lt;/code&gt;, &lt;code&gt;reserveBalance&lt;/code&gt;, and &lt;code&gt;curveSlope&lt;/code&gt; are correctly &lt;code&gt;export ledger&lt;/code&gt;. Anyone interacting with the contract needs to know the current price, and the price is a function of those three values. Hiding them would break the market.&lt;/p&gt;

&lt;h2&gt;
  
  
  World 2: Private state: the local world
&lt;/h2&gt;

&lt;p&gt;Private state is encrypted data stored locally by users, never exposed to the network.&lt;/p&gt;

&lt;p&gt;Private state in a Midnight contract lives on the user's machine, managed by the wallet SDK. It is never written to the chain as plaintext. The chain only ever sees a cryptographic commitment to it; a hash that proves the value exists and hasn't changed without revealing what it is.&lt;/p&gt;

&lt;p&gt;This is the right home for per-user data. Token balances, allowances, position sizes; information that belongs to the user and that nobody else has a legitimate reason to see.&lt;/p&gt;

&lt;p&gt;The contrast with &lt;code&gt;export ledger&lt;/code&gt; is stark:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;export ledger&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Private state&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where it lives&lt;/td&gt;
&lt;td&gt;Every node on the network&lt;/td&gt;
&lt;td&gt;User's local storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who can read it&lt;/td&gt;
&lt;td&gt;Anyone&lt;/td&gt;
&lt;td&gt;Only the owner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On-chain representation&lt;/td&gt;
&lt;td&gt;Plaintext value&lt;/td&gt;
&lt;td&gt;Cryptographic commitment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How it's updated&lt;/td&gt;
&lt;td&gt;Direct ledger assignment&lt;/td&gt;
&lt;td&gt;Via ZK proof&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solidity equivalent&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;public&lt;/code&gt; storage variable&lt;/td&gt;
&lt;td&gt;No equivalent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;disclose()&lt;/code&gt;: the boundary marker, not an escape hatch
&lt;/h2&gt;

&lt;p&gt;This is the most misunderstood operator in Compact, and understanding it precisely matters.&lt;/p&gt;

&lt;p&gt;A Compact program must explicitly declare its intention to disclose data that might be private before storing it in the public ledger, returning it from an exported circuit, or passing it to another contract.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;disclose()&lt;/code&gt; is not an encryption function. It is not a privacy mechanism. It does the opposite: it is a compile-time annotation that says &lt;em&gt;"I know this witness data is going public, and I am doing it on purpose."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Placing a &lt;code&gt;disclose()&lt;/code&gt; wrapper does not cause disclosure in itself; it has no effect other than telling the compiler that it is okay to disclose the value of the wrapped expression.&lt;/p&gt;

&lt;p&gt;Think of it as a signed waiver. You're not changing what happens at runtime, the value was going to the ledger either way. You're just telling the compiler you made a deliberate choice, not an accidental one.&lt;/p&gt;

&lt;p&gt;Without it, the compiler rejects your program outright:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;witness&lt;/span&gt; &lt;span class="nf"&gt;getBalance&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;recordBalance&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getBalance&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// compiler error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Exception: line 6 char 11:
  potential witness-value disclosure must be declared but is not:
    witness value potentially disclosed:
      the return value of witness getBalance at line 2 char 1
    nature of the disclosure:
      ledger operation might disclose the witness value
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;recordBalance&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getBalance&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;  &lt;span class="c1"&gt;// compiles fine&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compiler calls its enforcement mechanism the "witness protection program", it is implemented as an abstract interpreter, where the abstract values are not actual run-time values but information about witness data that will be contained within the actual run-time values. If at some point the interpreter encounters an undeclared disclosure of an abstract value containing witness data, the compiler halts and produces an appropriate error message.&lt;/p&gt;

&lt;p&gt;One subtlety worth knowing: the disclosure tracking follows witness data through arithmetic, type conversions, and function calls. You cannot obfuscate your way past it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;obfuscate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;73&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// still witness data&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;recordBalance&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;S&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getBalance&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;obfuscate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="py"&gt;.x&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// compiler still catches this&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Subjecting witness data to arithmetic, converting it from one representation to another, and passing it into and out of other circuits does not hide potential disclosure from the compiler.&lt;/p&gt;

&lt;p&gt;The best practice from the docs: put the &lt;code&gt;disclose()&lt;/code&gt; wrapper as close to the disclosure point as possible to avoid accidental disclosure if the data travels along multiple paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  The commitment pattern: how private state actually works
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;export ledger&lt;/code&gt; puts values on-chain as plaintext, and private state keeps them off-chain entirely, you might wonder: how does the contract &lt;em&gt;verify&lt;/em&gt; anything about private state without seeing it?&lt;/p&gt;

&lt;p&gt;The answer is commitments. A commitment scheme hashes arbitrary data together with a random nonce. The result can be safely placed into the ledger's state without revealing the original data. At a later point, the commitment can be "opened" by revealing the value and nonce, or a contract can simply prove (assert) that it has the correct value and nonce without ever revealing them.&lt;/p&gt;

&lt;p&gt;The Compact standard library provides the tools for this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;circuit&lt;/span&gt; &lt;span class="n"&gt;transientHash&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;circuit&lt;/span&gt; &lt;span class="n"&gt;transientCommit&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;circuit&lt;/span&gt; &lt;span class="n"&gt;persistentHash&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;circuit&lt;/span&gt; &lt;span class="n"&gt;persistentCommit&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;transient*&lt;/code&gt; functions should only be used when the values are not kept in state, while &lt;code&gt;persistent*&lt;/code&gt; outputs are suitable for storage in a contract's ledger state.&lt;/p&gt;

&lt;p&gt;There's an important note on nonces: the nonce must not be reused. If it is, you can link the commitments with the same nonces and values. Nonce reuse breaks the privacy guarantee, two commitments with the same nonce and value are identical on-chain, letting an observer link them.&lt;/p&gt;

&lt;p&gt;One additional thing the compiler knows: &lt;code&gt;transientCommit(e)&lt;/code&gt; is treated as non-witness data even if &lt;code&gt;e&lt;/code&gt; contains witness data. &lt;code&gt;transientHash(e)&lt;/code&gt; is not: the hash output is still tracked as witness-tainted. This is because a commitment includes a random nonce that sufficiently hides the input, while a bare hash might not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applying this to the bonding curve
&lt;/h2&gt;

&lt;p&gt;Here's the current state of the bonding curve contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// currently: fully public&lt;/span&gt;
&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt; &lt;span class="n"&gt;balances&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt; &lt;span class="n"&gt;allowances&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the &lt;code&gt;buy&lt;/code&gt; circuit updates them like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;caller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;callerAddress&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="n"&gt;balances&lt;/span&gt;&lt;span class="nf"&gt;.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;prevBalance&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both the address and the new balance are &lt;code&gt;disclose()&lt;/code&gt;-d; explicitly pushed public. Anyone watching the chain can see who bought and how many tokens they now hold. This is identical to what you'd get on Ethereum.&lt;/p&gt;

&lt;p&gt;The refactor toward private balances involves three changes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Store a commitment in the ledger instead of the balance&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt; &lt;span class="n"&gt;balanceCommitments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The map key is still the user's address (which stays public, you need to look up commitments by user). The value is &lt;code&gt;persistentCommit(balance, nonce)&lt;/code&gt;, a hash of the balance and a random nonce.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Keep the actual balance in private state&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The user's wallet holds the plaintext balance and the nonce locally. These never touch the chain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. In the circuit, prove the old commitment and assert the new one&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;buy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxCost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...existing checks...&lt;/span&gt;

    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;caller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;callerAddress&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;prevBalance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;localBalance&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;      &lt;span class="c1"&gt;// witness: read from private state&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;nonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;localNonce&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;              &lt;span class="c1"&gt;// witness: read from private state&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;newNonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;freshNonce&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;           &lt;span class="c1"&gt;// witness: generate new nonce&lt;/span&gt;

    &lt;span class="c1"&gt;// verify the existing commitment matches what's on-chain&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;oldCommit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;persistentCommit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prevBalance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;balanceCommitments&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;oldCommit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Balance commitment mismatch"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// write the new commitment — balance updated, but value stays private&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;newBalance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prevBalance&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;balanceCommitments&lt;/span&gt;&lt;span class="nf"&gt;.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;persistentCommit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newBalance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newNonce&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What goes on-chain: the new commitment. What stays private: the actual balance and nonce. The ZK proof guarantees the arithmetic was done correctly without revealing the operands.&lt;/p&gt;

&lt;p&gt;This is the pattern &lt;code&gt;PROTOCOL.md&lt;/code&gt; describes and the current contract code doesn't yet implement. The core curve logic, the witness-verified cost calculation, the reserve invariant, the slippage protection; stays completely unchanged. Only the balance storage layer changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision table
&lt;/h2&gt;

&lt;p&gt;Here's the practical guide for any state field you're designing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If your data...&lt;/th&gt;
&lt;th&gt;Use...&lt;/th&gt;
&lt;th&gt;Because...&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Defines market price or global invariants&lt;/td&gt;
&lt;td&gt;&lt;code&gt;export ledger&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Every participant needs it to interact correctly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Needs to be read by your frontend directly&lt;/td&gt;
&lt;td&gt;&lt;code&gt;export ledger&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Private state isn't accessible from TypeScript without a proof&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is per-user and sensitive&lt;/td&gt;
&lt;td&gt;Private state + commitment in ledger&lt;/td&gt;
&lt;td&gt;Balance stays local; proof verifies correctness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is a temporary computation input&lt;/td&gt;
&lt;td&gt;Witness (no ledger at all)&lt;/td&gt;
&lt;td&gt;Lives only for the duration of proof generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proves membership without revealing value&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;persistentCommit&lt;/code&gt; in &lt;code&gt;export ledger&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Commitment on-chain, value stays private&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is an identity/address you're intentionally publishing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;disclose()&lt;/code&gt; + &lt;code&gt;export ledger&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Explicit, deliberate public disclosure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The quick heuristic: if removing this field from the chain would break another user's ability to interact with the contract, it belongs in &lt;code&gt;export ledger&lt;/code&gt;. If it belongs only to one user and no one else needs to verify it directly, it belongs in private state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the compiler enforces this
&lt;/h2&gt;

&lt;p&gt;It's worth appreciating what Midnight's design actually achieves here. On Ethereum, accidentally making private data public is easy; you just expose the wrong storage variable. There's no language-level guardrail. The compiler is indifferent to privacy.&lt;/p&gt;

&lt;p&gt;Compact's "witness protection program" makes accidental disclosure a compile error. The decision to disclose private information must rest with each Midnight dApp because disclosure requirements are inherently situation-specific. Because private information should be disclosed only as necessary, Midnight's Compact language requires disclosure to be explicitly declared.&lt;/p&gt;

&lt;p&gt;Every &lt;code&gt;disclose()&lt;/code&gt; in your codebase is a record of a conscious decision. You can audit your contract's privacy model by grepping for &lt;code&gt;disclose()&lt;/code&gt;, every hit is a place where witness data crosses into the public world. If you see one you didn't intend, the fix is architectural, not cosmetic.&lt;/p&gt;

&lt;p&gt;That's a fundamentally different relationship between language and privacy than anything in the EVM ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;p&gt;The bonding curve commitment refactor outlined above is a genuine open contribution on &lt;a href="https://github.com/sevryn-labs/midnight-bonding-curve" rel="noopener noreferrer"&gt;the repo&lt;/a&gt;. If you're exploring Midnight's private state primitives, that's a contained, well-tested codebase to experiment on; the test suite already covers the invariants, so you'll know immediately if your refactor breaks something.&lt;/p&gt;

&lt;p&gt;The next level from here is understanding how private state interacts with transfers between users, which requires both parties to update their local private state atomically, coordinated by a proof that neither can fake. That's a more involved pattern and worth its own article.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Part 1: &lt;a href="https://dev.to/tusharpamnani/building-a-bonding-curve-token-on-midnight-with-real-zk-proofs-2h0i"&gt;Building a Bonding Curve Token on Midnight&lt;/a&gt;&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Full source: &lt;a href="https://github.com/sevryn-labs/midnight-bonding-curve" rel="noopener noreferrer"&gt;github.com/sevryn-labs/midnight-bonding-curve&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>midnight</category>
      <category>zeroknowledge</category>
      <category>blockchain</category>
      <category>web3</category>
    </item>
    <item>
      <title>Building a Bonding Curve Token on Midnight, With Real ZK Proofs</title>
      <dc:creator>Tushar Pamnani</dc:creator>
      <pubDate>Tue, 17 Mar 2026 13:59:53 +0000</pubDate>
      <link>https://dev.to/midnight-aliit/building-a-bonding-curve-token-on-midnight-with-real-zk-proofs-2h0i</link>
      <guid>https://dev.to/midnight-aliit/building-a-bonding-curve-token-on-midnight-with-real-zk-proofs-2h0i</guid>
      <description>&lt;p&gt;&lt;em&gt;Full source: &lt;a href="https://github.com/sevryn-labs/midnight-bonding-curve" rel="noopener noreferrer"&gt;github.com/sevryn-labs/midnight-bonding-curve&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you've built DeFi on EVM chains before, you already know the shape of a bonding curve: a smart contract that prices tokens algorithmically against supply, mints on buy, burns on sell, and holds a reserve. No order book. No oracle. The math is the market maker.&lt;/p&gt;

&lt;p&gt;What makes building one on Midnight interesting isn't that this implementation hides all your data - it doesn't, and we'll be precise about what's public and what isn't. What's interesting is the &lt;em&gt;execution model&lt;/em&gt;: how Midnight separates off-chain computation from on-chain verification using ZK proofs, and what that unlocks as a foundation for genuinely private DeFi primitives.&lt;/p&gt;

&lt;p&gt;We'll cover the bonding curve math, how Midnight's execution model differs from what you're used to, the ZK witness pattern that replaces what would normally just be on-chain arithmetic, and an honest account of the current privacy boundary. The full contract is in Compact (Midnight's smart contract language) and the off-chain layer is TypeScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  The math first, briefly
&lt;/h2&gt;

&lt;p&gt;We're using a linear bonding curve where the marginal price at supply &lt;code&gt;s&lt;/code&gt; is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P(s) = a · s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;a&lt;/code&gt; is the slope set at deployment. Price scales linearly with supply — simple, predictable, well-understood economic properties.&lt;/p&gt;

&lt;p&gt;Because price moves during a purchase, you can't just multiply. You integrate the curve over the range of tokens being bought:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nc"&gt;Cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;·&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="err"&gt;²&lt;/span&gt; &lt;span class="err"&gt;−&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="err"&gt;²&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// buying n tokens from supply s&lt;/span&gt;
&lt;span class="nc"&gt;Refund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;·&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="err"&gt;²&lt;/span&gt; &lt;span class="err"&gt;−&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="err"&gt;−&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="err"&gt;²&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// selling n tokens from supply s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From this falls a reserve invariant that must hold at all times:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;·&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="err"&gt;²&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every buy and sell is checked against this invariant inside the circuit. Any state that violates it cannot produce a valid ZK proof; the math enforces solvency at the cryptographic level.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;If you want the full derivation and economic intuition behind the curve, I wrote a deeper breakdown on &lt;a href="https://tusharpamnani7.hashnode.dev/bonding-curve-math" rel="noopener noreferrer"&gt;my blog →&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Midnight is actually doing differently
&lt;/h2&gt;

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

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

&lt;p&gt;Before diving into the contract, it's worth being precise about Midnight's execution model, because it's genuinely different from EVM or Solana and the differences shape every design decision.&lt;/p&gt;

&lt;p&gt;On a typical chain, when you call a function the validators re-execute your computation and check the result. Everyone sees the inputs. On Midnight, &lt;strong&gt;you execute the circuit locally, generate a ZK proof that you did it correctly, and submit only the proof plus public outputs&lt;/strong&gt;. Validators verify the proof in milliseconds without ever seeing your private inputs.&lt;/p&gt;

&lt;p&gt;The state model reflects this split directly. The first diagram above shows the full ZK transaction flow. The second shows the state split as it exists in this contract. &lt;code&gt;totalSupply&lt;/code&gt;, &lt;code&gt;reserveBalance&lt;/code&gt;, &lt;code&gt;curveSlope&lt;/code&gt;, and the admin fields are public &lt;code&gt;export ledger&lt;/code&gt; variables; anyone can read them on-chain. The witnesses, &lt;code&gt;callerAddress&lt;/code&gt; and &lt;code&gt;calculateCost&lt;/code&gt;, are computed locally and fed into the proof generation step but never written to the chain as plaintext.&lt;/p&gt;

&lt;p&gt;The proof server is a local Docker container. It never touches the network. It takes your inputs, runs the circuit constraints, and generates the proof. Your witness values never leave your machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  An honest account of what's public in this contract
&lt;/h2&gt;

&lt;p&gt;This is the section that most implementations gloss over, so let's be direct.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;balances&lt;/code&gt; and &lt;code&gt;allowances&lt;/code&gt; are defined as &lt;code&gt;export ledger&lt;/code&gt; maps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt; &lt;span class="n"&gt;balances&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;ledger&lt;/span&gt; &lt;span class="n"&gt;allowances&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Compact, &lt;code&gt;ledger&lt;/code&gt; variables represent public state synchronized across all nodes. And in the &lt;code&gt;buy&lt;/code&gt;, &lt;code&gt;sell&lt;/code&gt;, and &lt;code&gt;transfer&lt;/code&gt; circuits, balances are updated using &lt;code&gt;disclose()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;balances&lt;/span&gt;&lt;span class="nf"&gt;.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;prevBalance&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;disclose()&lt;/code&gt; explicitly adds a value to the transaction's public circuit outputs. That means updated balances and associated addresses are &lt;strong&gt;visible to anyone observing the blockchain&lt;/strong&gt;. This is the same privacy model as a standard EVM token.&lt;/p&gt;

&lt;p&gt;It's worth noting that &lt;code&gt;PROTOCOL.md&lt;/code&gt; in the repo describes a commitment-based privacy model for balances, but the current contract code doesn't implement it yet. Consider this a working reference implementation of the bonding curve mechanics, with private balances as the natural next step once the pattern is wired in.&lt;/p&gt;

&lt;p&gt;Midnight absolutely supports genuinely private balance maps using persistent private state and commitment types. This contract just doesn't use them yet. When someone asks "are token balances private on this contract?" — the honest answer right now is no, but the architecture of the chain makes it achievable without changing the core curve logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The witness pattern, where EVM and Midnight diverge most sharply
&lt;/h2&gt;

&lt;p&gt;This is the part that most confuses developers coming from Solidity, and it's also the most interesting part of the architecture.&lt;/p&gt;

&lt;p&gt;In Solidity, you'd compute the bonding curve cost directly inside the contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function mintCost(uint256 s, uint256 n) internal view returns (uint256) {
    return (slope * ((s + n)**2 - s**2)) / 2;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Compact, &lt;strong&gt;you can't do arbitrary arithmetic inside the circuit for free&lt;/strong&gt;. ZK circuits have constraints, and squaring large integers is expensive inside them. The solution is the &lt;strong&gt;witness pattern&lt;/strong&gt;: compute the expensive thing off-chain in TypeScript, pass the result into the circuit, and have the circuit verify the result rather than recompute it.&lt;/p&gt;

&lt;p&gt;The contract declares the witness as a foreign function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;witness&lt;/span&gt; &lt;span class="nf"&gt;calculateCost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;slope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;s_old&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;s_new&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You implement this in TypeScript using native &lt;code&gt;BigInt&lt;/code&gt;, no circuit constraints, full precision:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/math.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculateMintCost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;bigint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sNew&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slope&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sNew&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;sNew&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The circuit then verifies the result rather than trusting it blindly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;verifiedHalfProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;slope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;s_old&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;s_new&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculateCost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s_old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s_new&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;totalProduct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;slope&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_new&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;s_new&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;s_old&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;s_old&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;totalProduct&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;totalProduct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"Witness cost value failed verification"&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;2 * result + 1&lt;/code&gt; branch deserves an explanation. Compact uses integer (truncating) division. When &lt;code&gt;deltaSq = (s+n)² − s²&lt;/code&gt; is odd, dividing by 2 truncates — the remainder is lost. The circuit permits both branches because either &lt;code&gt;2·result == totalProduct&lt;/code&gt; (even case) or &lt;code&gt;2·result + 1 == totalProduct&lt;/code&gt; (odd case, truncated). Critically, both &lt;code&gt;Cost(s,n)&lt;/code&gt; and &lt;code&gt;Refund(s,n)&lt;/code&gt; for the same &lt;code&gt;(s,n)&lt;/code&gt; pair truncate the same product, so mint/burn reversibility is preserved across rounding.&lt;/p&gt;

&lt;p&gt;This is not a trust assumption, it's a constraint. A witness that provides a wrong value will fail this assertion and no valid proof can be generated.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;callerAddress()&lt;/code&gt;, the &lt;code&gt;msg.sender&lt;/code&gt; equivalent
&lt;/h2&gt;

&lt;p&gt;The other witness in the contract is &lt;code&gt;callerAddress()&lt;/code&gt;, declared as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;witness&lt;/span&gt; &lt;span class="nf"&gt;callerAddress&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the Midnight equivalent of Solidity's &lt;code&gt;msg.sender&lt;/code&gt;. It's implemented in &lt;code&gt;witnesses.ts&lt;/code&gt; to read the &lt;code&gt;address&lt;/code&gt; field out of the user's local &lt;code&gt;PrivateState&lt;/code&gt;, the wallet's identity, resolved off-chain during proof generation.&lt;/p&gt;

&lt;p&gt;Because it's a witness, you might wonder: can a user just lie and provide someone else's address? No. The value is bound by the ZK proof. In deployment, the circuit verifies that the provided witness is consistent with the public key that authorized the state transition; a user can only move tokens that their own proof can account for. The witness is provided by the user, but the circuit constraints make fraud cryptographically impossible to prove.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;buy&lt;/code&gt;, the caller address is used like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;caller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;callerAddress&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="n"&gt;balances&lt;/span&gt;&lt;span class="nf"&gt;.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;prevBalance&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;disclose()&lt;/code&gt; on &lt;code&gt;callerAddress()&lt;/code&gt; is what makes the buyer's address visible on-chain, consistent with the public balance model the contract currently uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Walking through a buy
&lt;/h2&gt;

&lt;p&gt;Here's the full &lt;code&gt;buy&lt;/code&gt; circuit with annotations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;buy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxCost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Emergency halt check&lt;/span&gt;
    &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;paused&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Contract is paused"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Optional supply cap enforcement&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;totalSupply&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;newSupply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;supplyCap&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;newSupply&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;supplyCap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Supply cap exceeded"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Cost via verified witness (off-chain compute, on-chain verify)&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verifiedHalfProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;curveSlope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newSupply&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 4. Slippage protection — caller set their own limit&lt;/span&gt;
    &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;maxCost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Cost exceeds maxCost slippage limit"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 5. Mint: update supply, reserve, and caller's balance&lt;/span&gt;
    &lt;span class="n"&gt;totalSupply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newSupply&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;reserveBalance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reserveBalance&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;caller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;callerAddress&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;balances&lt;/span&gt;&lt;span class="nf"&gt;.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;prevBalance&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;sell&lt;/code&gt; circuit mirrors this exactly, using &lt;code&gt;verifiedHalfProduct(curveSlope, newSupply, s)&lt;/code&gt; (arguments reversed) to compute the refund, and a &lt;code&gt;minRefund&lt;/code&gt; guard instead of &lt;code&gt;maxCost&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you actually deploy and run
&lt;/h2&gt;

&lt;p&gt;The repo structure maps cleanly to the execution model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;contracts/
  bonding_curve.compact       # Compact source → compiled to ZK circuits + TS bindings

src/
  math.ts                     # Off-chain witness implementations (pure BigInt)
  simulator.ts                # In-process state replica for testing (no proof server needed)
  cli.ts                      # Interactive buy/sell/transfer terminal
  deploy.ts                   # Deployment to Midnight Preprod
  witnesses.ts                # Witness implementations including callerAddress()
  tests/
    bonding_curve.test.ts     # 27 test sections, fuzz and invariant tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compilation turns &lt;code&gt;bonding_curve.compact&lt;/code&gt; into ZK circuits, generating keys and TypeScript API bindings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run compile   &lt;span class="c"&gt;# compactc → zkir/ + keys/ + TS bindings&lt;/span&gt;
npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To deploy or interact with a live contract, the proof server must be running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run start-proof-server   &lt;span class="c"&gt;# local Docker container, keep it alive&lt;/span&gt;

npm run deploy               &lt;span class="c"&gt;# deploys to Midnight Preprod, prompts for wallet seed&lt;/span&gt;
npm run cli                  &lt;span class="c"&gt;# interactive: buy, sell, transfer, inspect state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your wallet needs tNIGHT tokens for gas. Use the Midnight preprod faucet, then the CLI prompts you through everything else.&lt;/p&gt;

&lt;p&gt;One implementation detail worth calling out: the CLI uses &lt;strong&gt;Midnight Unshielded Addresses&lt;/strong&gt; (&lt;code&gt;mn_addr_...&lt;/code&gt;) as the canonical user identity. These Bech32m strings are decoded and SHA-256 hashed to produce a deterministic 32-byte key for the internal maps. This normalization prevents the same user from appearing as different identities depending on which address format they use.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test suite is worth reading
&lt;/h2&gt;

&lt;p&gt;The contract ships with 27 test sections. A few worth calling out:&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;reserve invariant tests&lt;/strong&gt; verify &lt;code&gt;R = (a/2) · s²&lt;/code&gt; after every operation across interleaved multi-user sequences. The &lt;strong&gt;integer arithmetic tests&lt;/strong&gt; specifically probe the odd/even &lt;code&gt;deltaSq&lt;/code&gt; truncation behavior and confirm that mint/burn round-trips are symmetric. The &lt;strong&gt;randomized fuzz&lt;/strong&gt; runs 100 random buy/sell sequences and checks the invariant after each round. The &lt;strong&gt;large-number stress tests&lt;/strong&gt; push supply values to 10¹¹ to surface overflow before it surfaces in production.&lt;/p&gt;

&lt;p&gt;If you're deploying a variant, the overflow section in the README deserves careful attention: at &lt;code&gt;slope=10&lt;/code&gt;, the reserve overflows &lt;code&gt;Uint&amp;lt;64&amp;gt;&lt;/code&gt; around &lt;code&gt;s ≈ 1.36 × 10⁹&lt;/code&gt;. Choosing slope and cap values such that &lt;code&gt;slope · cap² / 2 &amp;lt; 2⁶⁴&lt;/code&gt; is a deployment-time decision, not something the circuit enforces for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go from here
&lt;/h2&gt;

&lt;p&gt;The natural next step is refactoring &lt;code&gt;balances&lt;/code&gt; and &lt;code&gt;allowances&lt;/code&gt; to use Midnight's persistent private state - replacing the current &lt;code&gt;export ledger&lt;/code&gt; maps and &lt;code&gt;disclose()&lt;/code&gt; calls with commitment-backed private storage. The core curve logic, the witness pattern, and the reserve invariant all stay exactly the same. Only the state storage layer changes. That's the version that matches what &lt;code&gt;PROTOCOL.md&lt;/code&gt; describes, and it's a meaningful contribution to make to this repo if you're exploring Midnight's privacy primitives.&lt;/p&gt;

&lt;p&gt;Beyond that, the patterns here - witness-verified arithmetic, ZK-enforced reserve invariants, slippage protection at the circuit level, generalize naturally. Multi-token pools, TWAP oracles, prediction markets where positions are hidden until resolution: all of these become tractable once you've internalized the witness/circuit split.&lt;/p&gt;

&lt;p&gt;The full source, test suite, and deployment scripts are at &lt;strong&gt;&lt;a href="https://github.com/sevryn-labs/midnight-bonding-curve" rel="noopener noreferrer"&gt;github.com/sevryn-labs/midnight-bonding-curve&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Reference implementation, not audited, not production-ready without a full security review. See AUDIT.md in the repo before putting real value behind it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>web3</category>
      <category>midnight</category>
      <category>zeroknowledge</category>
    </item>
  </channel>
</rss>
