<?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: Harrie</title>
    <description>The latest articles on DEV Community by Harrie (@iamharrie).</description>
    <link>https://dev.to/iamharrie</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%2F3892012%2Ff502bedc-30e9-4bab-84b5-5d9c693b27c6.jpg</url>
      <title>DEV Community: Harrie</title>
      <link>https://dev.to/iamharrie</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/iamharrie"/>
    <language>en</language>
    <item>
      <title>Handling Midnight SDK Breaking Changes: A Developer's Upgrade Playbook</title>
      <dc:creator>Harrie</dc:creator>
      <pubDate>Tue, 12 May 2026 04:40:32 +0000</pubDate>
      <link>https://dev.to/iamharrie/handling-midnight-sdk-breaking-changes-a-developers-upgrade-playbook-1lcc</link>
      <guid>https://dev.to/iamharrie/handling-midnight-sdk-breaking-changes-a-developers-upgrade-playbook-1lcc</guid>
      <description>&lt;p&gt;You run &lt;code&gt;npm update&lt;/code&gt; on a Friday afternoon. Tests were passing that morning. Now your terminal looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CompactError: Version mismatch
  Expected circuit artifact version: 4.1.0
  Found: 3.8.2
  Contract: ./managed/counter/contract/index.cjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing is actually wrong with your &lt;code&gt;.compact&lt;/code&gt; source files. You just have stale compiled artifacts sitting in &lt;code&gt;managed/&lt;/code&gt; from before the upgrade, and the runtime now refuses to load them.&lt;/p&gt;

&lt;p&gt;Breaking changes on Midnight split into two categories: TypeScript API renames and Compact compiler artifact incompatibility. Once you know which you're dealing with, the fix is usually fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in the v3.x to v4.x migration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Package consolidation
&lt;/h3&gt;

&lt;p&gt;The biggest structural change was flattening six individual packages into one barrel export. If you haven't done this migration yet, your &lt;code&gt;package.json&lt;/code&gt; probably looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@midnight-ntwrk/wallet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3.2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@midnight-ntwrk/wallet-api"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3.2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@midnight-ntwrk/zswap"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3.2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@midnight-ntwrk/midnight-js-network-id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3.2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@midnight-ntwrk/midnight-js-types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3.2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@midnight-ntwrk/midnight-js-contracts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3.2.0"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After v4.0.3, one package replaces all of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@midnight-ntwrk/midnight-js"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^4.0.3"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your imports change accordingly:&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;// Before&lt;/span&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-ntwrk/wallet&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NetworkId&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-ntwrk/midnight-js-network-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ContractAddress&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-ntwrk/midnight-js-types&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// After&lt;/span&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="nx"&gt;NetworkId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ContractAddress&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-ntwrk/midnight-js&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;h3&gt;
  
  
  API renames
&lt;/h3&gt;

&lt;p&gt;Three function-level changes trip people up most.&lt;/p&gt;

&lt;p&gt;Wallet initialization renamed one provider option:&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;// Before&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wallet&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;Wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;walletProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;storageProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;networkId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NetworkId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TestNet&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// After&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wallet&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;Wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;privateStoragePasswordProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;storageProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;networkId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NetworkId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TestNet&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TypeScript catches &lt;code&gt;walletProvider&lt;/code&gt; immediately with "Object literal may only specify known properties." One of the easier breaks to find.&lt;/p&gt;

&lt;p&gt;Transaction signing merged two calls into 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="c1"&gt;// Before&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;balanced&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;balanceTx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&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="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;signTx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;balanced&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;await&lt;/span&gt; &lt;span class="nf"&gt;submitTx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// After&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;balanceAndSign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&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;await&lt;/span&gt; &lt;span class="nf"&gt;submitTx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Grep for &lt;code&gt;balanceTx&lt;/code&gt; to find every instance. It tends to show up in more places than expected.&lt;/p&gt;

&lt;p&gt;The CLI command changed from &lt;code&gt;compact compile&lt;/code&gt; to &lt;code&gt;compactc&lt;/code&gt;. CI pipelines and build scripts using the old command will either fail silently or error out, depending on how your shell handles missing executables.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pragma version
&lt;/h3&gt;

&lt;p&gt;Compact contracts now require an explicit version pragma at the top of every &lt;code&gt;.compact&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma language_version &amp;gt;= 0.20;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Contracts without this line fail at compile time with &lt;code&gt;CompactError: Language version not specified&lt;/code&gt;. Add it as the first non-comment line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnosing CompactError: Version mismatch
&lt;/h2&gt;

&lt;p&gt;When you compile a &lt;code&gt;.compact&lt;/code&gt; file, the Compact compiler generates circuit artifacts: &lt;code&gt;.cjs&lt;/code&gt; JavaScript wrappers and &lt;code&gt;.wasm&lt;/code&gt; zero-knowledge circuit binaries. These files contain an embedded version tag. When the SDK runtime (now v4.x) finds artifacts tagged for v3.x, it refuses to load them.&lt;/p&gt;

&lt;p&gt;Upgrading the SDK packages doesn't touch your compiled artifacts. They stay in &lt;code&gt;managed/&lt;/code&gt; exactly as they were. The runtime just stops accepting them.&lt;/p&gt;

&lt;p&gt;To confirm which contracts are affected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# List all contract artifact directories&lt;/span&gt;
&lt;span class="nb"&gt;ls &lt;/span&gt;managed/

&lt;span class="c"&gt;# Check the version inside one&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;managed/counter/contract/package.json | &lt;span class="nb"&gt;grep &lt;/span&gt;version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In practice, if you upgraded the SDK, assume all contracts are affected and recompile everything. Checking individually isn't worth the time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing the version mismatch
&lt;/h2&gt;

&lt;p&gt;Three steps, in order.&lt;/p&gt;

&lt;p&gt;Delete the stale artifacts first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; managed/&lt;span class="k"&gt;*&lt;/span&gt;/contract/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then recompile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Single contract&lt;/span&gt;
compactc src/contracts/counter.compact managed/counter/contract

&lt;span class="c"&gt;# All contracts at once&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;contract &lt;span class="k"&gt;in &lt;/span&gt;src/contracts/&lt;span class="k"&gt;*&lt;/span&gt;.compact&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$contract&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; .compact&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"managed/&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;/contract"&lt;/span&gt;
  compactc &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$contract&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"managed/&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;/contract"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the artifacts exist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;managed/counter/contract/
&lt;span class="c"&gt;# index.cjs  index.d.cts  zkir.wasm  ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see those files, the compile worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dependency audit workflow
&lt;/h2&gt;

&lt;p&gt;The order matters more than most people expect.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Audit before touching anything:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# What's currently installed&lt;/span&gt;
npm list | &lt;span class="nb"&gt;grep &lt;/span&gt;midnight

&lt;span class="c"&gt;# What updates are available&lt;/span&gt;
npm outdated | &lt;span class="nb"&gt;grep &lt;/span&gt;midnight
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the changelog before proceeding. Sometimes what looks like a routine upgrade removes a feature you're using.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Update &lt;code&gt;package.json&lt;/code&gt;. Replace old package names with the new barrel package. Remove old packages completely from both &lt;code&gt;dependencies&lt;/code&gt; and &lt;code&gt;devDependencies&lt;/code&gt;. Don't run install yet.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Delete &lt;code&gt;node_modules&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; node_modules
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;npm sometimes partially updates packages and leaves old peer dependency resolutions cached. A clean install avoids half-upgraded states where you're running v4 types against v3 implementations.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Delete compiled artifacts:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; managed/&lt;span class="k"&gt;*&lt;/span&gt;/contract/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do this before reinstalling. If you install first and forget to delete artifacts, you'll think the upgrade worked until the first test run.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install:
&lt;/li&gt;
&lt;/ol&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;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Recompile contracts:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;contract &lt;span class="k"&gt;in &lt;/span&gt;src/contracts/&lt;span class="k"&gt;*&lt;/span&gt;.compact&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$contract&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; .compact&lt;span class="si"&gt;)&lt;/span&gt;
  compactc &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$contract&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"managed/&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;/contract"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Run your test suite. TypeScript type errors from renamed imports surface here. Fix them as they come up.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The verified contract
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/IamHarrie-Labs/compact-sdk-upgrade-playbook" rel="noopener noreferrer"&gt;companion repository&lt;/a&gt; for this guide contains a minimal versioned feature-flag registry compiled against &lt;code&gt;pragma language_version &amp;gt;= 0.20&lt;/code&gt;. &lt;a href="https://github.com/IamHarrie-Labs/compact-sdk-upgrade-playbook/actions/runs/25702848338" rel="noopener noreferrer"&gt;CI passes&lt;/a&gt; on the latest Compact toolchain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma language_version &amp;gt;= 0.20;
import CompactStandardLibrary;

export ledger owner: Bytes&amp;lt;32&amp;gt;;
export ledger contractVersion: Bytes&amp;lt;32&amp;gt;;
export ledger upgradeCount: Counter;
export ledger featureFlags: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Boolean&amp;gt;;

witness getOwnerSecret(): Bytes&amp;lt;32&amp;gt;;

circuit ownerKey(): 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, "registry:owner:v1"),
        getOwnerSecret()
    ]);
}

circuit requireOwner(): [] {
    assert(disclose(ownerKey()) == owner, "Owner only");
}

export circuit initialize(ownerPubkey: Bytes&amp;lt;32&amp;gt;): [] {
    owner = disclose(ownerPubkey);
}

export circuit setVersion(newVersion: Bytes&amp;lt;32&amp;gt;): [] {
    requireOwner();
    contractVersion = disclose(newVersion);
    upgradeCount.increment(1);
}

export circuit enableFlag(flagKey: Bytes&amp;lt;32&amp;gt;): [] {
    requireOwner();
    featureFlags.insert(disclose(flagKey), disclose(true));
}

export circuit disableFlag(flagKey: Bytes&amp;lt;32&amp;gt;): [] {
    requireOwner();
    featureFlags.insert(disclose(flagKey), disclose(false));
}

export circuit isFlagEnabled(flagKey: Bytes&amp;lt;32&amp;gt;): Boolean {
    if (!featureFlags.member(disclose(flagKey))) {
        return false;
    }
    return featureFlags.lookup(disclose(flagKey));
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five exported circuits, well under Lace's 13-circuit deployment limit. The non-exported helpers (&lt;code&gt;ownerKey&lt;/code&gt;, &lt;code&gt;requireOwner&lt;/code&gt;) don't count toward the limit. Only &lt;code&gt;export circuit&lt;/code&gt; declarations do.&lt;/p&gt;

&lt;p&gt;The feature-flag pattern is genuinely useful post-upgrade. You can gate new features behind flags and enable them on-chain once you've confirmed the upgraded contracts are stable in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Upgrading packages without deleting artifacts.&lt;/strong&gt; The most common way to hit &lt;code&gt;CompactError: Version mismatch&lt;/code&gt;. Packages update, old &lt;code&gt;.wasm&lt;/code&gt; files stay. Delete and recompile after every SDK version change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Partial barrel migration.&lt;/strong&gt; Updating &lt;code&gt;package.json&lt;/code&gt; to use &lt;code&gt;@midnight-ntwrk/midnight-js&lt;/code&gt; but leaving old import paths in TypeScript files. This only works as long as the old packages are still in &lt;code&gt;dependencies&lt;/code&gt;. Remove them completely and run &lt;code&gt;npm list | grep @midnight-ntwrk&lt;/code&gt; after installing to confirm nothing old is lingering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Missing pragma.&lt;/strong&gt; Contracts compiled under older Compact versions don't have &lt;code&gt;pragma language_version &amp;gt;= 0.20;&lt;/code&gt; at the top. The compiler error is clear, but unexpected if you haven't seen it before. Add the pragma to every &lt;code&gt;.compact&lt;/code&gt; file as part of the upgrade.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;compactc&lt;/code&gt; not in PATH.&lt;/strong&gt; The toolchain manager (&lt;code&gt;compact&lt;/code&gt;) downloads the actual compiler binary. After updating the manager, run &lt;code&gt;compact update&lt;/code&gt; to pull the new compiler. If &lt;code&gt;compactc&lt;/code&gt; is still missing afterward, check that &lt;code&gt;~/.compact/bin&lt;/code&gt; is in your PATH.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reserved keywords as parameter names.&lt;/strong&gt; &lt;code&gt;from&lt;/code&gt; and &lt;code&gt;to&lt;/code&gt; are reserved keywords in Compact and can't be used as circuit parameter names. If you're refactoring transfer circuits during an upgrade, rename &lt;code&gt;from&lt;/code&gt; to &lt;code&gt;sender&lt;/code&gt; and &lt;code&gt;to&lt;/code&gt; to &lt;code&gt;receiver&lt;/code&gt;. The error looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;parse error: found keyword "from" looking for a typed pattern or ")"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one doesn't come from the SDK change itself, but tends to surface when people are reworking circuits during an upgrade.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stale type definitions from mixed packages.&lt;/strong&gt; If you add &lt;code&gt;@midnight-ntwrk/midnight-js&lt;/code&gt; without removing the individual packages, TypeScript picks up conflicting type definitions. The build might succeed, but you'll have subtle mismatches at runtime. Remove old packages completely.&lt;/p&gt;

&lt;h2&gt;
  
  
  That's the playbook
&lt;/h2&gt;

&lt;p&gt;Breaking SDK changes feel arbitrary until you've seen the pattern twice. The package consolidation is a one-time migration. &lt;code&gt;CompactError: Version mismatch&lt;/code&gt; always means the same thing: delete artifacts and recompile. The API renames are search-and-replace.&lt;/p&gt;

&lt;p&gt;The workflow: audit dependencies first, update package.json, delete node_modules and artifacts, clean install, recompile, fix imports. Same order every time. Skipping step 3 or 4 is how you spend four hours debugging a two-minute fix.&lt;/p&gt;

</description>
      <category>midnightfordevs</category>
      <category>blockchain</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Contract size limits on Midnight: what breaks when your dApp grows too complex</title>
      <dc:creator>Harrie</dc:creator>
      <pubDate>Mon, 11 May 2026 22:59:28 +0000</pubDate>
      <link>https://dev.to/iamharrie/contract-size-limits-on-midnight-what-breaks-when-your-dapp-grows-too-complex-57dm</link>
      <guid>https://dev.to/iamharrie/contract-size-limits-on-midnight-what-breaks-when-your-dapp-grows-too-complex-57dm</guid>
      <description>&lt;p&gt;You write a clean contract. It works. Then you add governance. Then staking. Then rewards. Then you deploy and hit a wall.&lt;/p&gt;

&lt;p&gt;Midnight has real limits on contract complexity. They don't all surface the same way — some kill deployment, some kill individual transactions, and some just make users wait while proofs grind. This article covers all three, with compiler-verified contracts showing what actually works.&lt;/p&gt;

&lt;p&gt;All three contracts compile against the latest Compact compiler. Verified CI run: &lt;a href="https://github.com/IamHarrie-Labs/compact-size-limits-guide/actions/runs/25701998943" rel="noopener noreferrer"&gt;IamHarrie-Labs/compact-size-limits-guide&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The three limits
&lt;/h2&gt;

&lt;p&gt;Lace's 13-circuit deployment limit is a hard cap enforced by the wallet at deploy time. Exceed 13 exported circuits and deployment is rejected. This is wallet-level, not protocol-level, which means it could change with a wallet update — but for now, 13 is the number.&lt;/p&gt;

&lt;p&gt;Block weight limits (error 1010) are a runtime problem. Midnight blocks measure transactions across five dimensions: readTime, computeTime, blockUsage, bytesWritten, and bytesChurned. If any of them blow past block capacity, the transaction is rejected. The contract deployed fine; this particular call just does too much.&lt;/p&gt;

&lt;p&gt;Proof generation time is the quieter problem. ZK proofs are generated client-side before submission. Constraint count drives proof time. Expensive operations — persistent hash calls, Map reads and writes, bounded loops — each add constraints. A heavy circuit can take several seconds to prove. In a UI, that's visible.&lt;/p&gt;

&lt;p&gt;These are three separate problems. The fixes are different.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 13-circuit limit
&lt;/h2&gt;

&lt;p&gt;Only &lt;code&gt;export circuit&lt;/code&gt; declarations count. Non-exported helper circuits, declared with just &lt;code&gt;circuit&lt;/code&gt;, are invisible to the limit. That's the most useful thing to understand here, because it's what makes the main optimization strategy work.&lt;/p&gt;

&lt;p&gt;Here's a monolithic contract pushing against it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// monolithic.compact
// Token + governance + staking in one contract.
// 12 exported circuits — one short of Lace's 13-circuit deployment limit.
//
// Non-exported helper circuits (adminKey, requireAdmin) do NOT count
// toward the 13-circuit limit. Only `export circuit` declarations count.

pragma language_version &amp;gt;= 0.20;
import CompactStandardLibrary;

// ─── Token state ───────────────────────────────────────────────────────────

export ledger admin: Bytes&amp;lt;32&amp;gt;;
export ledger balances: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Uint&amp;lt;64&amp;gt;&amp;gt;;
export ledger totalMinted: Counter;

// ─── Governance state ──────────────────────────────────────────────────────

export ledger proposals: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Boolean&amp;gt;;
export ledger voteCounts: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Uint&amp;lt;64&amp;gt;&amp;gt;;
export ledger hasVoted: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Boolean&amp;gt;;
export ledger proposalCount: Counter;

// ─── Staking state ─────────────────────────────────────────────────────────

export ledger stakedBalances: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Uint&amp;lt;64&amp;gt;&amp;gt;;
export ledger rewardPoints: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Uint&amp;lt;64&amp;gt;&amp;gt;;
export ledger totalStaked: Counter;

// ─── Witnesses ─────────────────────────────────────────────────────────────

witness getAdminSecret(): Bytes&amp;lt;32&amp;gt;;

// ─── Non-exported helpers (do NOT count toward the 13-circuit limit) ────────

circuit adminKey(): 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, "mono:admin:v1"),
        getAdminSecret()
    ]);
}

circuit requireAdmin(): [] {
    assert(disclose(adminKey()) == admin, "Admin only");
}

// ─── Exported circuits — each one counts toward Lace's 13-circuit limit ────
//     Count shown inline. At 13, deployment via Lace wallet will be rejected.

export circuit initialize(adminPubkey: Bytes&amp;lt;32&amp;gt;): [] {          // 1
    admin = disclose(adminPubkey);
}

// Token ──────────────────────────────────────────────────────────────────────

export circuit mint(                                             // 2
    recipient: Bytes&amp;lt;32&amp;gt;,
    amount: Uint&amp;lt;64&amp;gt;,
    newBalance: Uint&amp;lt;64&amp;gt;
): [] {
    requireAdmin();
    if (!balances.member(disclose(recipient))) {
        assert(disclose(newBalance) == disclose(amount), "First mint: balance must equal amount");
    } else {
        const current = balances.lookup(disclose(recipient));
        assert(disclose(newBalance) &amp;gt; current, "Balance must increase");
        assert(disclose(newBalance) - current == disclose(amount), "Invalid mint amount");
    }
    balances.insert(disclose(recipient), disclose(newBalance));
    totalMinted.increment(1);
}

export circuit transfer(                                         // 3
    sender: Bytes&amp;lt;32&amp;gt;,
    recipient2: Bytes&amp;lt;32&amp;gt;,
    amount: Uint&amp;lt;64&amp;gt;,
    newFromBalance: Uint&amp;lt;64&amp;gt;,
    newToBalance: Uint&amp;lt;64&amp;gt;
): [] {
    assert(balances.member(disclose(sender)), "Sender has no balance");
    const fromCurrent = balances.lookup(disclose(sender));
    assert(fromCurrent &amp;gt;= disclose(amount), "Insufficient balance");
    assert(disclose(newFromBalance) == fromCurrent - disclose(amount), "Invalid sender balance");
    if (!balances.member(disclose(recipient2))) {
        assert(disclose(newToBalance) == disclose(amount), "First receive: balance must equal amount");
    } else {
        const toCurrent = balances.lookup(disclose(recipient2));
        assert(disclose(newToBalance) &amp;gt; toCurrent, "Recipient balance must increase");
        assert(disclose(newToBalance) - toCurrent == disclose(amount), "Invalid recipient balance");
    }
    balances.insert(disclose(sender), disclose(newFromBalance));
    balances.insert(disclose(recipient2), disclose(newToBalance));
}

export circuit burn(userKey: Bytes&amp;lt;32&amp;gt;, amount: Uint&amp;lt;64&amp;gt;): [] {  // 4
    assert(balances.member(disclose(userKey)), "No balance");
    const current = balances.lookup(disclose(userKey));
    assert(current &amp;gt;= disclose(amount), "Insufficient balance");
    balances.insert(disclose(userKey), current - disclose(amount));
}

export circuit balanceOf(userKey: Bytes&amp;lt;32&amp;gt;): Uint&amp;lt;64&amp;gt; {         // 5
    if (!balances.member(disclose(userKey))) {
        return 0;
    }
    return balances.lookup(disclose(userKey));
}

// Governance ─────────────────────────────────────────────────────────────────

export circuit createProposal(proposalId: Bytes&amp;lt;32&amp;gt;): [] {       // 6
    requireAdmin();
    assert(!proposals.member(disclose(proposalId)), "Proposal already exists");
    proposals.insert(disclose(proposalId), disclose(true));
    proposalCount.increment(1);
}

export circuit vote(                                             // 7
    proposalId: Bytes&amp;lt;32&amp;gt;,
    userKey: Bytes&amp;lt;32&amp;gt;,
    newVoteCount: Uint&amp;lt;64&amp;gt;
): [] {
    assert(proposals.member(disclose(proposalId)), "Proposal does not exist");
    const voteKey = persistentHash&amp;lt;Vector&amp;lt;2, Bytes&amp;lt;32&amp;gt;&amp;gt;&amp;gt;([
        disclose(proposalId),
        disclose(userKey)
    ]);
    assert(!hasVoted.member(voteKey), "Already voted");
    if (voteCounts.member(disclose(proposalId))) {
        const current = voteCounts.lookup(disclose(proposalId));
        assert(disclose(newVoteCount) &amp;gt; current, "Vote count must increase");
    }
    hasVoted.insert(voteKey, disclose(true));
    voteCounts.insert(disclose(proposalId), disclose(newVoteCount));
}

export circuit voteCountOf(proposalId: Bytes&amp;lt;32&amp;gt;): Uint&amp;lt;64&amp;gt; {   // 8
    if (!voteCounts.member(disclose(proposalId))) {
        return 0;
    }
    return voteCounts.lookup(disclose(proposalId));
}

// Staking ────────────────────────────────────────────────────────────────────

export circuit stake(                                            // 9
    userKey: Bytes&amp;lt;32&amp;gt;,
    amount: Uint&amp;lt;64&amp;gt;,
    newBalance: Uint&amp;lt;64&amp;gt;
): [] {
    if (!stakedBalances.member(disclose(userKey))) {
        assert(disclose(newBalance) == disclose(amount), "First stake: balance must equal amount");
    } else {
        const current = stakedBalances.lookup(disclose(userKey));
        assert(disclose(newBalance) &amp;gt; current, "Staked balance must increase");
        assert(disclose(newBalance) - current == disclose(amount), "Invalid stake amount");
    }
    stakedBalances.insert(disclose(userKey), disclose(newBalance));
    totalStaked.increment(1);
}

export circuit unstake(userKey: Bytes&amp;lt;32&amp;gt;, amount: Uint&amp;lt;64&amp;gt;): [] { // 10
    assert(stakedBalances.member(disclose(userKey)), "No stake");
    const current = stakedBalances.lookup(disclose(userKey));
    assert(current &amp;gt;= disclose(amount), "Cannot unstake more than staked");
    stakedBalances.insert(disclose(userKey), current - disclose(amount));
}

export circuit stakedOf(userKey: Bytes&amp;lt;32&amp;gt;): Uint&amp;lt;64&amp;gt; {          // 11
    if (!stakedBalances.member(disclose(userKey))) {
        return 0;
    }
    return stakedBalances.lookup(disclose(userKey));
}

export circuit awardPoints(                                      // 12
    userKey: Bytes&amp;lt;32&amp;gt;,
    amount: Uint&amp;lt;64&amp;gt;,
    newBalance: Uint&amp;lt;64&amp;gt;
): [] {
    requireAdmin();
    if (!rewardPoints.member(disclose(userKey))) {
        assert(disclose(newBalance) == disclose(amount), "First award: balance must equal amount");
    } else {
        const current = rewardPoints.lookup(disclose(userKey));
        assert(disclose(newBalance) &amp;gt; current, "Balance must increase");
        assert(disclose(newBalance) - current == disclose(amount), "Invalid award amount");
    }
    rewardPoints.insert(disclose(userKey), disclose(newBalance));
}

// Circuit count: 12 / 13 maximum.
// Adding one more export circuit here would push this to the Lace limit.
// Any new feature domain requires splitting into a separate contract.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full working contract is in the repo. The count is the point: 12 exported circuits. &lt;code&gt;adminKey()&lt;/code&gt; and &lt;code&gt;requireAdmin()&lt;/code&gt; are helpers and don't count. Two more feature circuits, and this contract won't deploy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Block weight and error 1010
&lt;/h2&gt;

&lt;p&gt;Error 1010 is a runtime failure, not a deployment one. The contract is on-chain; this particular call just does too much in one transaction.&lt;/p&gt;

&lt;p&gt;Midnight measures block weight across five dimensions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;What it tracks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;readTime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cost of reading ledger state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;computeTime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Circuit computation cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;blockUsage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Overall block space consumed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bytesWritten&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;New bytes written to ledger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bytesChurned&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bytes written then immediately overwritten&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A circuit that reads from several Map keys, writes multiple entries, and calls &lt;code&gt;persistentHash&lt;/code&gt; a few times can hit this even with a completely reasonable circuit count. The fix is the same as for proof time: keep circuits focused.&lt;/p&gt;

&lt;p&gt;Operations with the most weight: &lt;code&gt;persistentHash&lt;/code&gt; adds significant constraint cost per call. &lt;code&gt;Map.lookup()&lt;/code&gt; and &lt;code&gt;Map.insert()&lt;/code&gt; each cost readTime or bytesWritten. Bounded loops scale with iteration count times body cost. Large &lt;code&gt;Bytes&amp;lt;N&amp;gt;&lt;/code&gt; values directly increase bytesWritten.&lt;/p&gt;

&lt;p&gt;The transfer circuit in &lt;code&gt;monolithic.compact&lt;/code&gt; does four Map operations. Fine for one transfer. A circuit running ten transfers in a loop is a different story.&lt;/p&gt;




&lt;h2&gt;
  
  
  Strategy 1: non-exported helper circuits
&lt;/h2&gt;

&lt;p&gt;Pulling repeated logic into non-exported circuits is the cheapest fix. It costs nothing in circuit count.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ Repeated in every admin-gated circuit — same hash computation every time
export circuit mint(...): [] {
    assert(disclose(persistentHash&amp;lt;Vector&amp;lt;2, Bytes&amp;lt;32&amp;gt;&amp;gt;&amp;gt;([
        pad(32, "token:admin:v1"),
        getAdminSecret()
    ])) == admin, "Admin only");
    // ... rest of circuit
}

export circuit createProposal(...): [] {
    assert(disclose(persistentHash&amp;lt;Vector&amp;lt;2, Bytes&amp;lt;32&amp;gt;&amp;gt;&amp;gt;([
        pad(32, "token:admin:v1"),
        getAdminSecret()
    ])) == admin, "Admin only");
    // ... rest of circuit
}
&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;// ✅ Extract into a non-exported helper — doesn't count toward 13-circuit limit
circuit adminKey(): 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, "token:admin:v1"),
        getAdminSecret()
    ]);
}

circuit requireAdmin(): [] {
    assert(disclose(adminKey()) == admin, "Admin only");
}

export circuit mint(...): [] {
    requireAdmin();
    // ... rest of circuit
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;adminKey()&lt;/code&gt; and &lt;code&gt;requireAdmin()&lt;/code&gt; don't appear in the circuit count. They're inlined at compile time, so there's no runtime cost either. Key derivation, bounds checks, member guards — anything repeated across exported circuits belongs in a helper.&lt;/p&gt;




&lt;h2&gt;
  
  
  Strategy 2: split by domain
&lt;/h2&gt;

&lt;p&gt;Once you've pulled out all the helpers and the circuit count is still too high, split by domain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;token.compact&lt;/strong&gt; — 5 exported circuits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// token.compact
// Token operations split into their own contract.
// 5 exported circuits — well under Lace's 13-circuit limit.

pragma language_version &amp;gt;= 0.20;
import CompactStandardLibrary;

export ledger admin: Bytes&amp;lt;32&amp;gt;;
export ledger balances: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Uint&amp;lt;64&amp;gt;&amp;gt;;
export ledger totalMinted: Counter;

witness getAdminSecret(): Bytes&amp;lt;32&amp;gt;;

circuit adminKey(): 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, "token:admin:v1"),
        getAdminSecret()
    ]);
}

circuit requireAdmin(): [] {
    assert(disclose(adminKey()) == admin, "Admin only");
}

circuit requireMember(userKey: Bytes&amp;lt;32&amp;gt;): [] {
    assert(balances.member(userKey), "User has no balance");
}

export circuit initialize(adminPubkey: Bytes&amp;lt;32&amp;gt;): [] {          // 1
    admin = disclose(adminPubkey);
}

export circuit mint(                                             // 2
    recipient: Bytes&amp;lt;32&amp;gt;,
    amount: Uint&amp;lt;64&amp;gt;,
    newBalance: Uint&amp;lt;64&amp;gt;
): [] {
    requireAdmin();
    if (!balances.member(disclose(recipient))) {
        assert(disclose(newBalance) == disclose(amount), "First mint: balance must equal amount");
    } else {
        const current = balances.lookup(disclose(recipient));
        assert(disclose(newBalance) &amp;gt; current, "Balance must increase");
        assert(disclose(newBalance) - current == disclose(amount), "Invalid mint amount");
    }
    balances.insert(disclose(recipient), disclose(newBalance));
    totalMinted.increment(1);
}

export circuit transfer(                                         // 3
    sender: Bytes&amp;lt;32&amp;gt;,
    receiver: Bytes&amp;lt;32&amp;gt;,
    amount: Uint&amp;lt;64&amp;gt;,
    newFromBalance: Uint&amp;lt;64&amp;gt;,
    newToBalance: Uint&amp;lt;64&amp;gt;
): [] {
    assert(balances.member(disclose(sender)), "Sender has no balance");
    const fromCurrent = balances.lookup(disclose(sender));
    assert(fromCurrent &amp;gt;= disclose(amount), "Insufficient balance");
    assert(disclose(newFromBalance) == fromCurrent - disclose(amount), "Invalid sender balance");
    if (!balances.member(disclose(receiver))) {
        assert(disclose(newToBalance) == disclose(amount), "First receive: balance must equal amount");
    } else {
        const toCurrent = balances.lookup(disclose(receiver));
        assert(disclose(newToBalance) &amp;gt; toCurrent, "Recipient balance must increase");
        assert(disclose(newToBalance) - toCurrent == disclose(amount), "Invalid recipient balance");
    }
    balances.insert(disclose(sender), disclose(newFromBalance));
    balances.insert(disclose(receiver), disclose(newToBalance));
}

export circuit burn(userKey: Bytes&amp;lt;32&amp;gt;, amount: Uint&amp;lt;64&amp;gt;): [] {  // 4
    assert(balances.member(disclose(userKey)), "No balance to burn");
    const current = balances.lookup(disclose(userKey));
    assert(current &amp;gt;= disclose(amount), "Insufficient balance");
    balances.insert(disclose(userKey), current - disclose(amount));
}

export circuit balanceOf(userKey: Bytes&amp;lt;32&amp;gt;): Uint&amp;lt;64&amp;gt; {         // 5
    if (!balances.member(disclose(userKey))) {
        return 0;
    }
    return balances.lookup(disclose(userKey));
}
// Total: 5 / 13
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;governance.compact&lt;/strong&gt; — 4 exported circuits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// governance.compact
// Governance operations in a separate contract from token logic.
// 4 exported circuits — well under Lace's 13-circuit limit.

pragma language_version &amp;gt;= 0.20;
import CompactStandardLibrary;

export ledger admin: Bytes&amp;lt;32&amp;gt;;
export ledger proposals: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Boolean&amp;gt;;
export ledger voteCounts: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Uint&amp;lt;64&amp;gt;&amp;gt;;
export ledger hasVoted: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Boolean&amp;gt;;
export ledger proposalCount: Counter;

witness getAdminSecret(): Bytes&amp;lt;32&amp;gt;;

circuit adminKey(): 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, "governance:admin:v1"),
        getAdminSecret()
    ]);
}

circuit requireAdmin(): [] {
    assert(disclose(adminKey()) == admin, "Admin only");
}

// Composite key for per-user per-proposal vote tracking.
circuit voteKey(proposalId: Bytes&amp;lt;32&amp;gt;, userKey: 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;([proposalId, userKey]);
}

export circuit initialize(adminPubkey: Bytes&amp;lt;32&amp;gt;): [] {          // 1
    admin = disclose(adminPubkey);
}

export circuit createProposal(proposalId: Bytes&amp;lt;32&amp;gt;): [] {       // 2
    requireAdmin();
    assert(!proposals.member(disclose(proposalId)), "Proposal already exists");
    proposals.insert(disclose(proposalId), disclose(true));
    proposalCount.increment(1);
}

export circuit vote(                                             // 3
    proposalId: Bytes&amp;lt;32&amp;gt;,
    userKey: Bytes&amp;lt;32&amp;gt;,
    newVoteCount: Uint&amp;lt;64&amp;gt;
): [] {
    assert(proposals.member(disclose(proposalId)), "Proposal does not exist");
    const key = voteKey(disclose(proposalId), disclose(userKey));
    assert(!hasVoted.member(key), "Already voted on this proposal");
    if (voteCounts.member(disclose(proposalId))) {
        const current = voteCounts.lookup(disclose(proposalId));
        assert(disclose(newVoteCount) &amp;gt; current, "Vote count must increase");
    }
    hasVoted.insert(key, disclose(true));
    voteCounts.insert(disclose(proposalId), disclose(newVoteCount));
}

export circuit voteCountOf(proposalId: Bytes&amp;lt;32&amp;gt;): Uint&amp;lt;64&amp;gt; {   // 4
    if (!voteCounts.member(disclose(proposalId))) {
        return 0;
    }
    return voteCounts.lookup(disclose(proposalId));
}
// Total: 4 / 13
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each deploys independently, each well under the limit. When staking comes next, that's a third contract, not a rewrite.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cross-contract coordination
&lt;/h2&gt;

&lt;p&gt;Compact circuits don't call each other. Coordination is the TypeScript layer's job. Call each contract in sequence, feeding the output of one into the input of the next.&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;// TypeScript coordinates calls across both contracts&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;voteWithTokenCheck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;proposalId&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="nx"&gt;voterKey&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="nx"&gt;tokenContractAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ContractAddress&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. Query token contract to verify the voter holds tokens&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;balance&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;tokenContract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;balanceOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;voterKey&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;balance&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&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;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;Must hold tokens to vote&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="c1"&gt;// 2. Get current vote count from governance contract&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentCount&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;governanceContract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;voteCountOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;proposalId&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;newVoteCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentCount&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Submit vote&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;governanceContract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;callTx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;proposalId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;voterKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newVoteCount&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;If you want an on-chain record of which token contract the governance contract is paired with, store the address in a ledger field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export ledger tokenContractRef: Bytes&amp;lt;32&amp;gt;;

export circuit setTokenContract(ref: Bytes&amp;lt;32&amp;gt;): [] {
    requireAdmin();
    tokenContractRef = disclose(ref);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual balance check still happens off-chain. The stored address is just an audit trail.&lt;/p&gt;




&lt;h2&gt;
  
  
  Strategy 3: keep individual circuits lean
&lt;/h2&gt;

&lt;p&gt;A heavy circuit is a problem regardless of circuit count. Proof time and block weight both scale with what a circuit does.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;persistentHash&lt;/code&gt; adds significant constraints per call. Factor repeated hash computations into non-exported helpers rather than duplicating them in every circuit that needs a key.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Map.lookup()&lt;/code&gt; and &lt;code&gt;Map.insert()&lt;/code&gt; each cost readTime or bytesWritten. A circuit that touches five Map keys is heavier than one that touches one. If a circuit is doing a lot of independent reads, consider whether some of that work can move off-chain.&lt;/p&gt;

&lt;p&gt;Off-chain delegation keeps constraint counts low. The &lt;code&gt;newBalance&lt;/code&gt; pattern used throughout these contracts is an example: TypeScript computes the arithmetic, the circuit just verifies the relationship. Keeping expensive computation in TypeScript and passing the result as a parameter is consistently cheaper than recomputing in-circuit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ In-circuit arithmetic widens the type and adds constraints
const newBalance = current + amount;  // Uint&amp;lt;64&amp;gt; + Uint&amp;lt;64&amp;gt; = Uint&amp;lt;1..2^65&amp;gt;

// ✅ Pass pre-computed value, verify in-circuit
export circuit mint(recipient: Bytes&amp;lt;32&amp;gt;, amount: Uint&amp;lt;64&amp;gt;, newBalance: Uint&amp;lt;64&amp;gt;): [] {
    const current = balances.lookup(disclose(recipient));
    assert(disclose(newBalance) - current == disclose(amount), "Invalid balance");
    balances.insert(disclose(recipient), disclose(newBalance));
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Pitfalls
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Counting witnesses and non-exported circuits toward the limit
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;witness getAdminSecret(): Bytes&amp;lt;32&amp;gt;;  // ← does NOT count
circuit adminKey(): Bytes&amp;lt;32&amp;gt; { ... } // ← does NOT count
export circuit mint(...): [] { ... }  // ← counts: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only &lt;code&gt;export circuit&lt;/code&gt; declarations count. &lt;code&gt;witness&lt;/code&gt; and bare &lt;code&gt;circuit&lt;/code&gt; are invisible to the 13-circuit limit.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Confusing the 13-circuit limit with error 1010
&lt;/h3&gt;

&lt;p&gt;They fail differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;13-circuit limit: deployment fails. Lace rejects the contract before it reaches the chain. Fix: split the contract or remove exported circuits.&lt;/li&gt;
&lt;li&gt;Error 1010: a specific transaction fails at runtime. The contract is deployed; this call is just too expensive. Fix: reduce what the circuit does per call, or split the work across multiple transactions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Calling &lt;code&gt;Map.lookup()&lt;/code&gt; without a &lt;code&gt;Map.member()&lt;/code&gt; guard
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ Panics at proof generation if the key doesn't exist
const current = voteCounts.lookup(disclose(proposalId));
&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;// ✅ Guard first
if (voteCounts.member(disclose(proposalId))) {
    const current = voteCounts.lookup(disclose(proposalId));
    // ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This applies inside helper circuits too. A helper that calls &lt;code&gt;lookup()&lt;/code&gt; unsafely will panic in every exported circuit that calls it.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. &lt;code&gt;Uint&amp;lt;64&amp;gt;&lt;/code&gt; arithmetic widening in-circuit
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ Uint&amp;lt;64&amp;gt; + Uint&amp;lt;64&amp;gt; = Uint&amp;lt;1..2^65&amp;gt; — can't store to Uint&amp;lt;64&amp;gt;
const newBalance = current + amount;
balances.insert(disclose(userKey), newBalance);
&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;expected right-hand side to have type Uint&amp;lt;64&amp;gt;
but received Uint&amp;lt;1..18446744073709551616&amp;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;// ✅ Compute off-chain, verify in-circuit using subtraction (type-safe)
export circuit mint(recipient: Bytes&amp;lt;32&amp;gt;, amount: Uint&amp;lt;64&amp;gt;, newBalance: Uint&amp;lt;64&amp;gt;): [] {
    const current = balances.lookup(disclose(recipient));
    assert(disclose(newBalance) - current == disclose(amount), "Invalid balance");
    balances.insert(disclose(recipient), disclose(newBalance));
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Subtraction stays within &lt;code&gt;Uint&amp;lt;64&amp;gt;&lt;/code&gt; range once you've asserted the operands are safe. Addition widens. This is consistent across all balance-tracking circuits in this article.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Missing &lt;code&gt;disclose()&lt;/code&gt; on circuit parameters used in assertions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ Compiler error
assert(amount &amp;gt; 0, "Amount must be positive");
&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;// ✅
assert(disclose(amount) &amp;gt; 0, "Amount must be positive");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Circuit parameters behave like witnesses from the type system's perspective. Any comparison, ledger write, or function call that would expose the value requires an explicit &lt;code&gt;disclose()&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Using reserved keywords as parameter names
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ Parse error — 'from' is a reserved keyword
export circuit transfer(from: Bytes&amp;lt;32&amp;gt;, to: Bytes&amp;lt;32&amp;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;parse error: found keyword "from" looking for a typed pattern or ")"
&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;// ✅ Use non-reserved names
export circuit transfer(sender: Bytes&amp;lt;32&amp;gt;, receiver: Bytes&amp;lt;32&amp;gt;, ...): [] { ... }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compact reserves words that look like valid identifiers: &lt;code&gt;from&lt;/code&gt;, &lt;code&gt;to&lt;/code&gt;, &lt;code&gt;import&lt;/code&gt;, &lt;code&gt;export&lt;/code&gt;, &lt;code&gt;ledger&lt;/code&gt;, &lt;code&gt;circuit&lt;/code&gt;, &lt;code&gt;witness&lt;/code&gt;, and others. The error ("found keyword X looking for a typed pattern") means the parser hit a reserved word where it expected a variable name. &lt;code&gt;sender&lt;/code&gt; and &lt;code&gt;receiver&lt;/code&gt; work fine.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Using &lt;code&gt;Counter.increment()&lt;/code&gt; with a &lt;code&gt;Uint&amp;lt;64&amp;gt;&lt;/code&gt; argument
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ Type error — Counter.increment expects Uint&amp;lt;16&amp;gt;
totalMinted.increment(disclose(amount));  // amount: Uint&amp;lt;64&amp;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;expected first argument of increment to have type Uint&amp;lt;16&amp;gt;
but received Uint&amp;lt;64&amp;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;// ✅ Pass a literal
totalMinted.increment(1);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Counter&lt;/code&gt; counts operations, not arbitrary sums. For large aggregates, use a &lt;code&gt;Map&lt;/code&gt; field.&lt;/p&gt;




&lt;h2&gt;
  
  
  Compiler-verified source
&lt;/h2&gt;

&lt;p&gt;All three contracts — &lt;code&gt;monolithic.compact&lt;/code&gt;, &lt;code&gt;token.compact&lt;/code&gt;, and &lt;code&gt;governance.compact&lt;/code&gt; — compile against the latest Compact compiler:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/IamHarrie-Labs/compact-size-limits-guide/actions/runs/25701998943" rel="noopener noreferrer"&gt;https://github.com/IamHarrie-Labs/compact-size-limits-guide/actions/runs/25701998943&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.midnight.network/develop/reference/compact/lang-ref" rel="noopener noreferrer"&gt;Compact Language Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.midnight.network/getting-started" rel="noopener noreferrer"&gt;Midnight Developer Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forum.midnight.network/" rel="noopener noreferrer"&gt;Midnight Developer Forum&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://discord.com/invite/midnightnetwork" rel="noopener noreferrer"&gt;Midnight Discord&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>midnightfordevs</category>
      <category>blockchain</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Contract-state accounting vs UTXO tokens: two models for onchain value on Midnight</title>
      <dc:creator>Harrie</dc:creator>
      <pubDate>Mon, 11 May 2026 22:32:31 +0000</pubDate>
      <link>https://dev.to/iamharrie/contract-state-accounting-vs-utxo-tokens-two-models-for-onchain-value-on-midnight-1n6j</link>
      <guid>https://dev.to/iamharrie/contract-state-accounting-vs-utxo-tokens-two-models-for-onchain-value-on-midnight-1n6j</guid>
      <description>&lt;p&gt;Midnight gives you two ways to represent value in a contract. Most tutorials pick one and move on. This one covers both: what the real compiler-verified API looks like, where each model breaks, and why the UTXO path has more gotchas than it appears.&lt;/p&gt;

&lt;p&gt;Short version: UTXO tokens for privacy and real transferability. Ledger-state accounting for queryable bookkeeping. Both, when your contract needs both.&lt;/p&gt;

&lt;p&gt;All three contracts here compile against the latest Compact compiler. Verified CI run: &lt;a href="https://github.com/IamHarrie-Labs/compact-token-guide/actions/runs/25700476519" rel="noopener noreferrer"&gt;IamHarrie-Labs/compact-token-guide&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Two separate systems
&lt;/h2&gt;

&lt;p&gt;Midnight runs a public ledger and a shielded Zswap layer. Not two views of the same data. Genuinely separate systems with different guarantees.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Counter&lt;/code&gt; and &lt;code&gt;Map&lt;/code&gt; fields live on the public ledger. Every insert, every increment is readable by anyone watching the chain. You can query balances, enumerate holders, build conditional logic on accumulated state. You cannot hide amounts.&lt;/p&gt;

&lt;p&gt;Zswap is a zero-knowledge Merkle tree. Tokens committed into it have their amounts, owners, and transfer history hidden. You can't ask "how much does Alice hold?" from inside a contract. The contract has no view into individual UTXO holdings. What it can do: mint coins, verify a coin is valid, send coins to recipients.&lt;/p&gt;

&lt;p&gt;Pick the wrong model and you'll have balances everyone can read when they shouldn't, or a contract that can't answer "how much does this user have?" That's often the entire point of the contract.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ledger-state accounting
&lt;/h2&gt;

&lt;p&gt;Ledger-state accounting uses &lt;code&gt;Counter&lt;/code&gt; and &lt;code&gt;Map&lt;/code&gt; fields to track values inside the contract. Think of it as an on-chain database where your contract is the only writer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma language_version &amp;gt;= 0.20;
import CompactStandardLibrary;

export ledger totalPoints: Counter;
export ledger userPoints: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Uint&amp;lt;64&amp;gt;&amp;gt;;

witness getAdminSecret(): Bytes&amp;lt;32&amp;gt;;

circuit adminKey(): 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, "accounting:admin:v1"),
        getAdminSecret()
    ]);
}

export ledger admin: Bytes&amp;lt;32&amp;gt;;

export circuit initialize(adminPubkey: Bytes&amp;lt;32&amp;gt;): [] {
    admin = disclose(adminPubkey);
}

export circuit awardPoints(
    userKey: Bytes&amp;lt;32&amp;gt;,
    amount: Uint&amp;lt;64&amp;gt;,
    newBalance: Uint&amp;lt;64&amp;gt;
): [] {
    assert(disclose(adminKey()) == admin, "Only admin can award points");

    if (!userPoints.member(disclose(userKey))) {
        assert(disclose(newBalance) == disclose(amount), "First award: balance must equal amount");
    } else {
        const current = userPoints.lookup(disclose(userKey));
        assert(disclose(newBalance) &amp;gt;= current, "Balance must not decrease");
        assert(disclose(newBalance) - current == disclose(amount), "Invalid balance update");
    }

    userPoints.insert(disclose(userKey), disclose(newBalance));
    totalPoints.increment(1);
}

export circuit redeemPoints(
    userKey: Bytes&amp;lt;32&amp;gt;,
    amount: Uint&amp;lt;64&amp;gt;
): [] {
    assert(userPoints.member(disclose(userKey)), "User has no points");

    const current = userPoints.lookup(disclose(userKey));
    assert(current &amp;gt;= disclose(amount), "Insufficient points");

    userPoints.insert(disclose(userKey), current - disclose(amount));
}

export circuit pointsOf(userKey: Bytes&amp;lt;32&amp;gt;): Uint&amp;lt;64&amp;gt; {
    if (!userPoints.member(disclose(userKey))) {
        return 0;
    }
    return userPoints.lookup(disclose(userKey));
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What you get
&lt;/h3&gt;

&lt;p&gt;Every balance is public. &lt;code&gt;userPoints["alice"]&lt;/code&gt; is readable by anyone watching the ledger. That's fine for loyalty points, reputation scores, game credits — anything where the value isn't sensitive. You can enumerate holders, check balances from other circuits, build vesting schedules and rate limits.&lt;/p&gt;

&lt;p&gt;The TypeScript call for &lt;code&gt;awardPoints&lt;/code&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="c1"&gt;// Compute new balance off-chain — arithmetic happens here, not in-circuit&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;current&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;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pointsOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userKey&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;newBalance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;callTx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;awardPoints&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newBalance&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Uint&amp;lt;64&amp;gt; arithmetic constraint
&lt;/h3&gt;

&lt;p&gt;The contract takes &lt;code&gt;newBalance&lt;/code&gt; as a parameter from TypeScript rather than computing it in-circuit. This is deliberate. It trips up most developers the first time they write a balance-tracking contract.&lt;/p&gt;

&lt;p&gt;When you add two &lt;code&gt;Uint&amp;lt;64&amp;gt;&lt;/code&gt; values in-circuit, the result type widens. &lt;code&gt;Uint&amp;lt;64&amp;gt; + Uint&amp;lt;64&amp;gt;&lt;/code&gt; produces &lt;code&gt;Uint&amp;lt;1..2^65&amp;gt;&lt;/code&gt;, which is too wide to store back into a &lt;code&gt;Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Uint&amp;lt;64&amp;gt;&amp;gt;&lt;/code&gt; field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ Type error: Uint&amp;lt;64&amp;gt; + Uint&amp;lt;64&amp;gt; = Uint&amp;lt;1..2^65&amp;gt;, not Uint&amp;lt;64&amp;gt;
const newBal = current + amount;
userPoints.insert(disclose(userKey), newBal);
&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;expected right-hand side to have type Uint&amp;lt;64&amp;gt;
but received Uint&amp;lt;1..18446744073709551616&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix: compute the result in TypeScript, pass it as a circuit parameter, verify the relationship in-circuit. TypeScript does the arithmetic. The circuit proves it was done correctly.&lt;/p&gt;

&lt;p&gt;Subtraction stays within range. &lt;code&gt;Uint&amp;lt;64&amp;gt; - Uint&amp;lt;64&amp;gt;&lt;/code&gt; is fine as long as the operand won't go negative, which the &lt;code&gt;assert(current &amp;gt;= amount)&lt;/code&gt; guarantees.&lt;/p&gt;

&lt;h3&gt;
  
  
  When to use it
&lt;/h3&gt;

&lt;p&gt;Use it for internal state that doesn't need to leave the contract, anything you need to query or compare from other circuits, or early development when you don't have a full Midnight node. Avoid it for anything with sensitive amounts.&lt;/p&gt;




&lt;h2&gt;
  
  
  UTXO-layer tokens
&lt;/h2&gt;

&lt;p&gt;UTXO tokens live in the Zswap Merkle tree, not in ledger fields. The contract is a policy layer: it governs who can create, receive, and send coins. Actual token custody is the shielded transaction engine's job.&lt;/p&gt;

&lt;h3&gt;
  
  
  The actual standard library API
&lt;/h3&gt;

&lt;p&gt;Most community examples get this wrong. The &lt;code&gt;mint(amount, recipient)&lt;/code&gt; you'll see in various tutorials isn't in the standard library. The real functions:&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="nf"&gt;mintShieldedToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;domainSep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Either&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ZswapCoinPublicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ContractAddress&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;ShieldedCoinInfo&lt;/span&gt;

&lt;span class="nf"&gt;sendShielded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;QualifiedShieldedCoinInfo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Either&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ZswapCoinPublicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ContractAddress&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;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;128&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;ShieldedSendResult&lt;/span&gt;

&lt;span class="nf"&gt;receiveShielded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;coin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ShieldedCoinInfo&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="nf"&gt;ownPublicKey&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;ZswapCoinPublicKey&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The types involved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ShieldedCoinInfo&lt;/code&gt; — a coin commitment: &lt;code&gt;{ nonce: Bytes&amp;lt;32&amp;gt;, color: Bytes&amp;lt;32&amp;gt;, value: Uint&amp;lt;128&amp;gt; }&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;QualifiedShieldedCoinInfo&lt;/code&gt; — a coin plus its Merkle tree position: &lt;code&gt;{ nonce: Bytes&amp;lt;32&amp;gt;, color: Bytes&amp;lt;32&amp;gt;, value: Uint&amp;lt;128&amp;gt;, mtIndex: Uint&amp;lt;64&amp;gt; }&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ZswapCoinPublicKey&lt;/code&gt; — a shielded wallet address: &lt;code&gt;{ bytes: Bytes&amp;lt;32&amp;gt; }&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Either&amp;lt;L, R&amp;gt;&lt;/code&gt; — use &lt;code&gt;left&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(v)&lt;/code&gt; for a wallet key&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These complex types come from the wallet SDK through witnesses. You can't pass &lt;code&gt;QualifiedShieldedCoinInfo&lt;/code&gt; as a direct circuit parameter. The wallet has to provide it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma language_version &amp;gt;= 0.20;
import CompactStandardLibrary;

export ledger admin: Bytes&amp;lt;32&amp;gt;;
export ledger totalMinted: Counter;

witness getAdminSecret(): Bytes&amp;lt;32&amp;gt;;
witness getMintNonce(): Bytes&amp;lt;32&amp;gt;;
witness getRecipient(): ZswapCoinPublicKey;
witness getQualifiedCoin(): QualifiedShieldedCoinInfo;
witness getCoinToReceive(): ShieldedCoinInfo;

circuit adminKey(): 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, "utxo:admin:v1"),
        getAdminSecret()
    ]);
}

export circuit initialize(adminPubkey: Bytes&amp;lt;32&amp;gt;): [] {
    admin = disclose(adminPubkey);
}

// Mint shielded tokens directly to a recipient's wallet.
// Domain separator scopes these tokens to this contract version.
export circuit mintToWallet(value: Uint&amp;lt;64&amp;gt;): [] {
    assert(disclose(adminKey()) == admin, "Only admin can mint");

    const recipient = getRecipient();
    const nonce = getMintNonce();

    mintShieldedToken(
        pad(32, "token:v1"),
        disclose(value),
        disclose(nonce),
        left&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(disclose(recipient))
    );

    totalMinted.increment(1);
}

// Transfer shielded tokens. Protocol handles double-spend prevention.
export circuit transferShielded(value: Uint&amp;lt;128&amp;gt;): [] {
    const coin = getQualifiedCoin();
    const recipient = getRecipient();

    sendShielded(
        disclose(coin),
        left&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(disclose(recipient)),
        disclose(value)
    );
}

// Receive shielded tokens into this contract.
export circuit receiveIntoContract(): [] {
    const coin = getCoinToReceive();
    receiveShielded(disclose(coin));
}

// Get this contract's shielded public key (use as recipient address).
export circuit contractPublicKey(): ZswapCoinPublicKey {
    return ownPublicKey();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How the Zswap layer works
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;mintShieldedToken&lt;/code&gt; creates a coin commitment in the Zswap Merkle tree. The coin's owner, amount, and nonce are hidden inside it. Anyone watching the chain can see a mint happened. Nothing else.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sendShielded&lt;/code&gt; takes a coin the caller already owns. The &lt;code&gt;QualifiedShieldedCoinInfo&lt;/code&gt; includes the Merkle tree index proving the coin exists and hasn't been spent. Old coin nullified, new commitment created for the recipient.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;receiveShielded&lt;/code&gt; is for when the contract itself is the recipient. It pulls the coin into the contract's UTXO holdings so the contract can send it onward later.&lt;/p&gt;

&lt;p&gt;Double-spend prevention is protocol-level. The Zswap nullifier set handles it. No application logic required.&lt;/p&gt;

&lt;h3&gt;
  
  
  When token operations get blocked
&lt;/h3&gt;

&lt;p&gt;UTXO operations need the full Midnight node and a Zswap-capable wallet. &lt;code&gt;mintShieldedToken&lt;/code&gt; fails in simulator-only environments. &lt;code&gt;sendShielded&lt;/code&gt; requires the wallet to provide a valid Merkle inclusion proof. If the wallet doesn't support it, the witness returns nothing and proof generation fails.&lt;/p&gt;

&lt;p&gt;Ledger-state accounting works in all environments. UTXO needs the full stack. That's the main practical reason to fall back to accounting even for something that conceptually feels like a token.&lt;/p&gt;

&lt;h3&gt;
  
  
  When to use it
&lt;/h3&gt;

&lt;p&gt;Use UTXO tokens when value needs to move between wallets privately, when the amounts themselves are sensitive, or when you'd rather not write your own double-spend logic. The tradeoff: you need the full Midnight stack, and the witness-based coin provisioning has its own learning curve.&lt;/p&gt;




&lt;h2&gt;
  
  
  Combining both: staking rewards
&lt;/h2&gt;

&lt;p&gt;Public participation tracking plus private reward distributions. Staking contracts are a natural fit for this split.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma language_version &amp;gt;= 0.20;
import CompactStandardLibrary;

// Public: anyone can see total staked and per-user stakes
export ledger admin: Bytes&amp;lt;32&amp;gt;;
export ledger totalStaked: Counter;
export ledger userStakes: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Uint&amp;lt;64&amp;gt;&amp;gt;;

// Public aggregate only — individual reward amounts are hidden in Zswap
export ledger totalRewarded: Counter;

witness getAdminSecret(): Bytes&amp;lt;32&amp;gt;;
witness getMintNonce(): Bytes&amp;lt;32&amp;gt;;
witness getRecipient(): ZswapCoinPublicKey;

circuit adminKey(): 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, "hybrid:admin:v1"),
        getAdminSecret()
    ]);
}

export circuit initialize(adminPubkey: Bytes&amp;lt;32&amp;gt;): [] {
    admin = disclose(adminPubkey);
}

// ACCOUNTING: record a stake
export circuit recordStake(
    userKey: Bytes&amp;lt;32&amp;gt;,
    amount: Uint&amp;lt;64&amp;gt;,
    newBalance: Uint&amp;lt;64&amp;gt;
): [] {
    if (!userStakes.member(disclose(userKey))) {
        assert(disclose(newBalance) == disclose(amount), "First stake: balance must equal amount");
    } else {
        const current = userStakes.lookup(disclose(userKey));
        assert(disclose(newBalance) &amp;gt; current, "Balance must increase");
        assert(disclose(newBalance) - current == disclose(amount), "Invalid stake amount");
    }

    userStakes.insert(disclose(userKey), disclose(newBalance));
    totalStaked.increment(1);
}

// ACCOUNTING: remove a stake
export circuit removeStake(
    userKey: Bytes&amp;lt;32&amp;gt;,
    amount: Uint&amp;lt;64&amp;gt;
): [] {
    assert(userStakes.member(disclose(userKey)), "No stake recorded");

    const current = userStakes.lookup(disclose(userKey));
    assert(current &amp;gt;= disclose(amount), "Cannot remove more than staked");

    userStakes.insert(disclose(userKey), current - disclose(amount));
}

// UTXO: distribute real shielded token rewards (private amounts)
export circuit distributeReward(rewardAmount: Uint&amp;lt;64&amp;gt;): [] {
    assert(disclose(adminKey()) == admin, "Only admin can distribute rewards");

    const recipient = getRecipient();
    const nonce = getMintNonce();

    mintShieldedToken(
        pad(32, "hybrid:reward:v1"),
        disclose(rewardAmount),
        disclose(nonce),
        left&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(disclose(recipient))
    );

    totalRewarded.increment(1);
}

export circuit stakeOf(userKey: Bytes&amp;lt;32&amp;gt;): Uint&amp;lt;64&amp;gt; {
    if (!userStakes.member(disclose(userKey))) {
        return 0;
    }
    return userStakes.lookup(disclose(userKey));
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The accounting circuits give auditors a clear view of who staked what. The UTXO circuit keeps reward amounts hidden. &lt;code&gt;totalRewarded&lt;/code&gt; increments on-chain, but who got what stays inside Zswap.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Loyalty points, credits, scores&lt;/td&gt;
&lt;td&gt;Accounting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Participation tracking, voting weight&lt;/td&gt;
&lt;td&gt;Accounting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internal rate limits or quotas&lt;/td&gt;
&lt;td&gt;Accounting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payment tokens, transferable assets&lt;/td&gt;
&lt;td&gt;UTXO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amount and participant privacy required&lt;/td&gt;
&lt;td&gt;UTXO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unit-testing without full Midnight node&lt;/td&gt;
&lt;td&gt;Accounting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internal tracking + real payouts&lt;/td&gt;
&lt;td&gt;Hybrid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you need to query balances inside the contract, UTXO won't work. The contract can't read Zswap holdings. If you need private transfers between wallets, ledger-state won't work. Those balances are fully public.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pitfalls
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;Map.get()&lt;/code&gt; doesn't exist — use &lt;code&gt;Map.lookup()&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ Compile error — Map has no .get() method
const balance = userPoints.get(disclose(userKey));
&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;// ✅ Correct
if (userPoints.member(disclose(userKey))) {
    const balance = userPoints.lookup(disclose(userKey));
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. &lt;code&gt;Map.lookup()&lt;/code&gt; panics on a missing key
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ Panics at proof generation if userKey was never inserted
const balance = userPoints.lookup(disclose(userKey));
&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;// ✅ Guard with .member() first
assert(userPoints.member(disclose(userKey)), "User has no balance");
const balance = userPoints.lookup(disclose(userKey));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. &lt;code&gt;Set&amp;lt;T&amp;gt;&lt;/code&gt; doesn't exist — use &lt;code&gt;Map&amp;lt;K, Boolean&amp;gt;&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ Compile error — Compact has no Set type
export ledger spentCoins: Set&amp;lt;Bytes&amp;lt;32&amp;gt;&amp;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;// ✅ Map&amp;lt;K, Boolean&amp;gt; is the set pattern
export ledger spentCoins: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Boolean&amp;gt;;
spentCoins.insert(disclose(coinId), disclose(true));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. &lt;code&gt;Uint&amp;lt;64&amp;gt;&lt;/code&gt; addition widens the type
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ Uint&amp;lt;64&amp;gt; + Uint&amp;lt;64&amp;gt; = Uint&amp;lt;1..2^65&amp;gt; — can't store back to Uint&amp;lt;64&amp;gt;
const newBalance = current + amount;
userPoints.insert(disclose(key), newBalance);
&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;expected right-hand side to have type Uint&amp;lt;64&amp;gt;
but received Uint&amp;lt;1..18446744073709551616&amp;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;// ✅ Compute off-chain, verify in-circuit
export circuit awardPoints(key: Bytes&amp;lt;32&amp;gt;, amount: Uint&amp;lt;64&amp;gt;, newBalance: Uint&amp;lt;64&amp;gt;): [] {
    const current = userPoints.lookup(disclose(key));
    assert(disclose(newBalance) - current == disclose(amount), "Invalid balance");
    userPoints.insert(disclose(key), disclose(newBalance));
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Missing &lt;code&gt;disclose()&lt;/code&gt; on exported parameters in comparisons
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ Compiler error
assert(amount &amp;gt; 0, "Amount must be positive");
&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;// ✅ Exported parameters need disclose() before comparisons
assert(disclose(amount) &amp;gt; 0, "Amount must be positive");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compact compiler error: &lt;code&gt;potential witness-value disclosure must be declared but is not — performing this ledger operation might disclose the boolean value of the result of a comparison involving the witness value&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Wrong UTXO API signatures
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ Not a standard library function
mint(amount, recipient);

// ❌ Wrong signature — sendShielded doesn't take (amount, recipient)
sendShielded(amount, recipient);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The correct forms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ✅ mintShieldedToken: domain separator + nonce + explicit type params on left()
mintShieldedToken(
    pad(32, "token:v1"),
    disclose(value),
    disclose(nonce),
    left&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(disclose(recipient))
);

// ✅ sendShielded: takes QualifiedShieldedCoinInfo from witness, not just an amount
sendShielded(
    disclose(coin),                                    // QualifiedShieldedCoinInfo
    left&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(disclose(recipient)),
    disclose(value)                                    // Uint&amp;lt;128&amp;gt;
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;mintShieldedToken&lt;/code&gt; needs a domain separator to scope the token type to this contract, a per-mint nonce to prevent replay, and &lt;code&gt;left()&lt;/code&gt; with explicit type parameters. Type inference doesn't work here.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. &lt;code&gt;Counter.increment()&lt;/code&gt; takes &lt;code&gt;Uint&amp;lt;16&amp;gt;&lt;/code&gt;, not &lt;code&gt;Uint&amp;lt;64&amp;gt;&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ Type error
totalPoints.increment(disclose(amount));  // amount: Uint&amp;lt;64&amp;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;expected first argument of increment to have type Uint&amp;lt;16&amp;gt;
but received Uint&amp;lt;64&amp;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;// ✅ Pass a literal
totalPoints.increment(1);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Counter.increment()&lt;/code&gt; accepts step values up to 65535. For large aggregate sums, use a &lt;code&gt;Map&lt;/code&gt; field. Use &lt;code&gt;Counter&lt;/code&gt; to count operations only.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Missing &lt;code&gt;disclose()&lt;/code&gt; on witness values passed to UTXO functions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ Compiler error: witness values passed to UTXO functions need disclose()
mintShieldedToken(pad(32, "token:v1"), disclose(value), nonce, left&amp;lt;...&amp;gt;(recipient));
sendShielded(coin, left&amp;lt;...&amp;gt;(recipient), disclose(value));
receiveShielded(coin);
&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;potential witness-value disclosure must be declared but is not:
  the call to standard-library circuit mintShieldedToken might disclose a link between
  a coin spend and the coin with the commitment given by a hash of the witness value
&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;// ✅ Disclose witness values before passing to any UTXO standard library function
mintShieldedToken(
    pad(32, "token:v1"),
    disclose(value),
    disclose(nonce),
    left&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(disclose(recipient))
);
sendShielded(
    disclose(coin),
    left&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;(disclose(recipient)),
    disclose(value)
);
receiveShielded(disclose(coin));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This applies to all three: &lt;code&gt;mintShieldedToken&lt;/code&gt;, &lt;code&gt;sendShielded&lt;/code&gt;, and &lt;code&gt;receiveShielded&lt;/code&gt;. Each creates or consumes a coin commitment that hashes your witness value on-chain. Compact requires explicit &lt;code&gt;disclose()&lt;/code&gt; to acknowledge that link. The private data stays inside the ZK proof. What you're acknowledging is that a hash of your witness will appear on-chain.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Treating accounting balances as private
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// This balance is publicly readable — NOT private
export ledger userPoints: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Uint&amp;lt;64&amp;gt;&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ledger fields are visible to anyone reading the chain. For sensitive amounts (stake sizes, bid values, holdings), use the UTXO model or commitment patterns with &lt;code&gt;persistentCommit&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Compiler-verified source
&lt;/h2&gt;

&lt;p&gt;All three contracts — &lt;code&gt;accounting.compact&lt;/code&gt;, &lt;code&gt;utxo-token.compact&lt;/code&gt;, and &lt;code&gt;hybrid.compact&lt;/code&gt; — compile against the latest Compact compiler:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/IamHarrie-Labs/compact-token-guide/actions/runs/25700476519" rel="noopener noreferrer"&gt;https://github.com/IamHarrie-Labs/compact-token-guide/actions/runs/25700476519&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.midnight.network/develop/reference/compact/lang-ref" rel="noopener noreferrer"&gt;Compact Language Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.midnight.network/compact/standard-library/exports" rel="noopener noreferrer"&gt;Standard Library: &lt;code&gt;mintShieldedToken&lt;/code&gt;, &lt;code&gt;sendShielded&lt;/code&gt;, &lt;code&gt;receiveShielded&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forum.midnight.network/" rel="noopener noreferrer"&gt;Midnight Developer Forum&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://discord.com/invite/midnightnetwork" rel="noopener noreferrer"&gt;Midnight Discord&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>midnighrfordevs</category>
      <category>blockchain</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Replay Attack Prevention in Compact: Nonces, Nullifiers, and Domain Separation</title>
      <dc:creator>Harrie</dc:creator>
      <pubDate>Mon, 11 May 2026 21:18:13 +0000</pubDate>
      <link>https://dev.to/iamharrie/replay-attack-prevention-in-compact-nonces-nullifiers-and-domain-separation-2nnh</link>
      <guid>https://dev.to/iamharrie/replay-attack-prevention-in-compact-nonces-nullifiers-and-domain-separation-2nnh</guid>
      <description>&lt;p&gt;On Ethereum, replay protection is built into the protocol. Every account has a nonce; every transaction carries it; the node rejects duplicates. You don't think about it.&lt;/p&gt;

&lt;p&gt;Midnight doesn't work that way. Transactions arrive as zero-knowledge proofs. The network verifies the proof is valid — but it doesn't inherently know whether that exact proof has been submitted before. The public ledger sees the result of a valid proof, not the transaction itself. That means replay prevention is your job, not the protocol's, and you have to design it explicitly into every circuit that needs it.&lt;/p&gt;

&lt;p&gt;This tutorial covers the three mechanisms Compact gives you: counter-based nonces, set-based nullifiers derived from &lt;code&gt;persistentCommit(secret, context)&lt;/code&gt;, and domain separation tags. Each one addresses a different class of replay attack, and knowing which to reach for — and when to combine them — is one of the more consequential design decisions in Compact contract development.&lt;/p&gt;

&lt;p&gt;All three contracts in this article compile against the latest Compact compiler. Verified CI run: &lt;a href="https://github.com/IamHarrie-Labs/compact-replay-prevention-guide/actions/runs/25690416194" rel="noopener noreferrer"&gt;IamHarrie-Labs/compact-replay-prevention-guide&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Three kinds of replay
&lt;/h2&gt;

&lt;p&gt;It helps to be precise about what you're defending against.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operation replay&lt;/strong&gt;: the same valid proof is submitted twice. A voter submits their vote, the transaction succeeds, an attacker captures the transaction and resubmits it. If the contract doesn't track which proofs have been consumed, the vote tallies twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-operation replay&lt;/strong&gt;: a proof valid for one circuit is reused in a different circuit. A governance contract has both a &lt;code&gt;castVote&lt;/code&gt; and a &lt;code&gt;delegateVote&lt;/code&gt; circuit. Without domain separation, if both circuits hash the same inputs, a proof generated for voting might satisfy the delegation circuit — an attacker extracts real capability from a proof they didn't generate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-contract replay&lt;/strong&gt;: a proof from contract A is replayed against contract B. Two different election contracts with the same circuit structure — a voter's proof for one election might be valid against the other if the nullifier isn't scoped to the specific contract.&lt;/p&gt;

&lt;p&gt;The three mechanisms map to these threats:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Threat&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Operation replay&lt;/td&gt;
&lt;td&gt;Counter nonces or set-based nullifiers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-operation replay&lt;/td&gt;
&lt;td&gt;Domain separation tags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-contract replay&lt;/td&gt;
&lt;td&gt;Context-scoped nullifiers + domain separation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Mechanism 1: Counter-based nonces
&lt;/h2&gt;

&lt;p&gt;A counter nonce assigns each participant a monotonically increasing number. Each operation must reference the current nonce; after a successful operation the nonce advances. Any replay of an old transaction carries an outdated nonce and fails immediately.&lt;/p&gt;

&lt;p&gt;This is the most transparent approach — nonces are public ledger state, so any participant can check their current value before submitting. It's the right choice when participants have known, stable identities and operations need strict ordering.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma language_version &amp;gt;= 0.20;
import CompactStandardLibrary;

export ledger userNonces: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Uint&amp;lt;64&amp;gt;&amp;gt;;
export ledger operationCount: Counter;

witness getUserKey(): Bytes&amp;lt;32&amp;gt;;

export circuit operate(
    userKey: Bytes&amp;lt;32&amp;gt;,
    nonce: Uint&amp;lt;64&amp;gt;,
    nextNonce: Uint&amp;lt;64&amp;gt;
): [] {
    if (!userNonces.member(disclose(userKey))) {
        assert(disclose(nonce) == 0, "First operation must use nonce 0");
    } else {
        assert(
            disclose(nonce) == userNonces.lookup(disclose(userKey)),
            "Invalid nonce — replay or out-of-order submission"
        );
    }

    assert(disclose(nextNonce) &amp;gt; disclose(nonce), "nextNonce must exceed current nonce");
    userNonces.insert(disclose(userKey), disclose(nextNonce));
    operationCount.increment(1);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things in this contract that will bite you if you copy-paste common Solidity patterns directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Boolean&amp;gt;&lt;/code&gt; not &lt;code&gt;Set&amp;lt;&amp;gt;&lt;/code&gt;&lt;/strong&gt; — Compact has no &lt;code&gt;Set&lt;/code&gt; type. Use &lt;code&gt;Map&amp;lt;K, Boolean&amp;gt;&lt;/code&gt; where you'd reach for a set. This applies to nullifier storage too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;member()&lt;/code&gt; before &lt;code&gt;lookup()&lt;/code&gt;&lt;/strong&gt; — calling &lt;code&gt;lookup&lt;/code&gt; on a key that doesn't exist in the map panics at proof generation. Always check &lt;code&gt;member&lt;/code&gt; first, or use &lt;code&gt;insertDefault&lt;/code&gt; to pre-populate. Missing this check means first-time users will hit an opaque failure instead of a clean error message.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;Uint&amp;lt;64&amp;gt;&lt;/code&gt; arithmetic constraint&lt;/strong&gt; — Compact's range types mean &lt;code&gt;nonce + 1&lt;/code&gt; on a &lt;code&gt;Uint&amp;lt;64&amp;gt;&lt;/code&gt; produces a &lt;code&gt;Uint&amp;lt;1..2^64&amp;gt;&lt;/code&gt;, which is wider than &lt;code&gt;Uint&amp;lt;64&amp;gt;&lt;/code&gt; and can't be assigned back to a ledger field. The fix is to pass both &lt;code&gt;nonce&lt;/code&gt; (current, to verify) and &lt;code&gt;nextNonce&lt;/code&gt; (computed off-chain, to store). The contract verifies strict ordering, and TypeScript handles the &lt;code&gt;+1&lt;/code&gt;. This pattern recurs whenever you'd naturally write &lt;code&gt;ledgerField = existingValue + constant&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who calls &lt;code&gt;operate&lt;/code&gt;?&lt;/strong&gt; The &lt;code&gt;userKey&lt;/code&gt; is an exported circuit parameter. It's what the caller claims their identity is. In a real contract, you'd derive &lt;code&gt;userKey&lt;/code&gt; from a private key via &lt;code&gt;ownPublicKey()&lt;/code&gt; or a witness, not accept it freely. The circuit above accepts it as a public input for simplicity.&lt;/p&gt;

&lt;h3&gt;
  
  
  When nonces fall short
&lt;/h3&gt;

&lt;p&gt;Counter nonces have one significant limitation: they serialize operations. If two transactions with the same nonce are in-flight simultaneously, only one succeeds — the other must regenerate with the updated nonce. For single-owner or low-throughput contracts this is fine. For high-concurrency systems (airdrops, voting with thousands of simultaneous participants), nullifiers are the better choice.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mechanism 2: Set-based nullifiers with &lt;code&gt;persistentCommit&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;A nullifier is a one-time-use token derived from a private secret. Once consumed on-chain, any subsequent attempt to reuse the same nullifier is rejected. Unlike nonces, nullifiers don't require a known identity — the secret stays private; only the nullifier hash appears on the ledger.&lt;/p&gt;

&lt;p&gt;The bounty specifies nullifiers derived from &lt;code&gt;persistentCommit(secret, context)&lt;/code&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;persistentCommit&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&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="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opening&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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="nx"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;v&lt;/code&gt; = the private secret (what the commitment binds to)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;opening&lt;/code&gt; = the context/domain string (what scopes the nullifier)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using a fixed context string as the opening makes the nullifier &lt;strong&gt;deterministic&lt;/strong&gt; (same inputs always produce the same output, which is what you need for replay detection) and &lt;strong&gt;scoped&lt;/strong&gt; (changing the context string produces a different nullifier, letting the same secret participate in different campaigns without cross-campaign collision).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma language_version &amp;gt;= 0.20;
import CompactStandardLibrary;

// Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Boolean&amp;gt; is Compact's set pattern — no dedicated Set type exists
export ledger spentNullifiers: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Boolean&amp;gt;;
export ledger totalClaims: Counter;

witness getSecret(): Bytes&amp;lt;32&amp;gt;;

export circuit claimReward(context: Bytes&amp;lt;32&amp;gt;): [] {
    const secret = getSecret();

    // Derive nullifier: persistentCommit(secret, context)
    // secret: private — binding, never on-chain
    // context: public — scopes this nullifier to a specific campaign
    const nullifier = persistentCommit&amp;lt;Bytes&amp;lt;32&amp;gt;&amp;gt;(secret, disclose(context));

    assert(
        !spentNullifiers.member(disclose(nullifier)),
        "Nullifier already spent: reward already claimed"
    );

    // Record nullifier BEFORE side effects — always
    spentNullifiers.insert(disclose(nullifier), disclose(true));

    totalClaims.increment(1);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;persistentCommit&lt;/code&gt; vs &lt;code&gt;persistentHash&lt;/code&gt; for nullifiers
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;persistentHash&amp;lt;T&amp;gt;(v: T): Bytes&amp;lt;32&amp;gt;&lt;/code&gt; is deterministic and produces a fixed hash. For a nullifier that just needs to be binding, it works. But it offers no built-in context scoping.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;persistentCommit&amp;lt;T&amp;gt;(v: T, opening: Bytes&amp;lt;32&amp;gt;): Bytes&amp;lt;32&amp;gt;&lt;/code&gt; accepts an explicit opening value. When you use a domain string as the opening, you get a naturally scoped nullifier: &lt;code&gt;persistentCommit(secret, pad(32, "airdrop:season-1:v1"))&lt;/code&gt; produces a different result from &lt;code&gt;persistentCommit(secret, pad(32, "airdrop:season-2:v1"))&lt;/code&gt;, even though the secret is identical. One secret, multiple campaigns, zero cross-campaign collision.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;persistentHash&lt;/code&gt; you'd have to concatenate manually and hope your encoding is consistent. &lt;code&gt;persistentCommit&lt;/code&gt; with a context argument makes the scoping explicit and type-safe.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context scoping prevents cross-contract replay
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;context&lt;/code&gt; parameter in &lt;code&gt;claimReward&lt;/code&gt; is publicly visible. If two different contracts both have a &lt;code&gt;claimReward&lt;/code&gt; circuit and a participant uses the same secret in both, the nullifier from contract A (&lt;code&gt;persistentCommit(secret, contextA)&lt;/code&gt;) differs from the nullifier in contract B (&lt;code&gt;persistentCommit(secret, contextB)&lt;/code&gt;) as long as the context strings differ. The context acts as the scope boundary.&lt;/p&gt;

&lt;p&gt;In practice, include the contract address or a unique deployment ID in the context:&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;// Off-chain: compute context with contract address for cross-contract safety&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encodeContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;airdrop:season-1:v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;contractAddress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Record nullifier before side effects
&lt;/h3&gt;

&lt;p&gt;In Compact, a circuit is atomic — it either fully succeeds or fully reverts. But the logical ordering still matters for clarity and security audits: mark the nullifier as spent &lt;em&gt;before&lt;/em&gt; executing side effects. If you do it the other way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ Side effect first — logically backwards
totalClaims.increment(1);
spentNullifiers.insert(disclose(nullifier), disclose(true)); // too late conceptually
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Auditors reading your contract will expect nullifier recording before state changes. More importantly, in future contract upgrades or more complex circuits where partial execution might become possible, the safe ordering protects you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Storage growth
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;spentNullifiers&lt;/code&gt; grows forever — one entry per claim, unbounded. For high-throughput or long-lived contracts, this is a real concern. Compact has no native pruning for maps; the current mitigation is designing campaigns with bounded participation (a fixed voter tree, a capped airdrop size) so the map growth is bounded by design. For contracts where unbounded growth is unavoidable, the off-chain client can query map size before executing and alert when it approaches the tree capacity.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mechanism 3: Domain separation tags
&lt;/h2&gt;

&lt;p&gt;Domain separation solves cross-operation replay. The threat: a contract has two circuits that hash similar inputs. Without distinct tags, a proof valid for circuit A might hash to the same value as circuit B, letting an attacker reuse one proof for a different operation.&lt;/p&gt;

&lt;p&gt;The fix is a unique prefix on every hash computation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;persistentHash&amp;lt;Vector&amp;lt;n, Bytes&amp;lt;32&amp;gt;&amp;gt;&amp;gt;([
    pad(32, "contract-name:operation:version"),
    ...data
])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if two circuits receive identical data inputs, different domain tags produce completely different outputs. The circuits become cryptographically isolated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma language_version &amp;gt;= 0.20;
import CompactStandardLibrary;

export ledger voteNullifiers: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Boolean&amp;gt;;
export ledger delegateNullifiers: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Boolean&amp;gt;;
export ledger totalVotes: Counter;
export ledger totalDelegations: Counter;

witness getVoterSecret(): Bytes&amp;lt;32&amp;gt;;

circuit voteNullifier(secret: Bytes&amp;lt;32&amp;gt;, proposalId: Bytes&amp;lt;32&amp;gt;): Bytes&amp;lt;32&amp;gt; {
    return persistentHash&amp;lt;Vector&amp;lt;3, Bytes&amp;lt;32&amp;gt;&amp;gt;&amp;gt;([
        pad(32, "gov:vote:v1"),       // ← vote-specific domain tag
        secret,
        proposalId
    ]);
}

circuit delegateNullifier(secret: Bytes&amp;lt;32&amp;gt;, proposalId: Bytes&amp;lt;32&amp;gt;): Bytes&amp;lt;32&amp;gt; {
    return persistentHash&amp;lt;Vector&amp;lt;3, Bytes&amp;lt;32&amp;gt;&amp;gt;&amp;gt;([
        pad(32, "gov:delegate:v1"),   // ← delegate-specific domain tag — different hash space
        secret,
        proposalId
    ]);
}

export circuit castVote(proposalId: Bytes&amp;lt;32&amp;gt;): [] {
    const secret = getVoterSecret();
    const nullifier = voteNullifier(secret, disclose(proposalId));

    assert(!voteNullifiers.member(disclose(nullifier)), "Vote already cast for this proposal");
    voteNullifiers.insert(disclose(nullifier), disclose(true));
    totalVotes.increment(1);
}

export circuit delegateVote(proposalId: Bytes&amp;lt;32&amp;gt;): [] {
    const secret = getVoterSecret();
    const nullifier = delegateNullifier(secret, disclose(proposalId));

    assert(!delegateNullifiers.member(disclose(nullifier)), "Already delegated for this proposal");
    delegateNullifiers.insert(disclose(nullifier), disclose(true));
    totalDelegations.increment(1);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same voter (same &lt;code&gt;secret&lt;/code&gt;) can both vote and delegate on the same proposal. The &lt;code&gt;voteNullifier&lt;/code&gt; and &lt;code&gt;delegateNullifier&lt;/code&gt; circuits produce different hashes for identical inputs because the tags differ. No cross-operation collision is possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Domain tag design
&lt;/h3&gt;

&lt;p&gt;Good tags follow a consistent pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{project}:{contract}:{operation}:v{version}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;"gov:proposal:nullifier:v1"&lt;/code&gt; — proposal submission in a governance contract&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"token:transfer:nullifier:v1"&lt;/code&gt; — transfer in a token contract&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"airdrop:claim:nullifier:v1"&lt;/code&gt; — airdrop claim&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The version suffix matters for upgrades. If you change the contract's logic and redeploy, existing nullifiers from the old version should remain valid or explicitly invalid — a version bump in the tag ensures the two contract generations don't share a hash space.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Always use &lt;code&gt;pad(32, tag)&lt;/code&gt;&lt;/strong&gt; to get a fixed 32-byte prefix. &lt;code&gt;pad&lt;/code&gt; left-pads the string with zeros. Without it, variable-length tags create ambiguity: &lt;code&gt;"a" || "bc"&lt;/code&gt; and &lt;code&gt;"ab" || "c"&lt;/code&gt; would produce the same byte sequence and could collide.&lt;/p&gt;

&lt;h3&gt;
  
  
  The official Midnight pattern
&lt;/h3&gt;

&lt;p&gt;Midnight's own Bulletin Board contract uses this exact approach:&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 publicKey(sk: Bytes&amp;lt;32&amp;gt;, sequence: Bytes&amp;lt;32&amp;gt;): Bytes&amp;lt;32&amp;gt; {
    return persistentHash&amp;lt;Vector&amp;lt;3, Bytes&amp;lt;32&amp;gt;&amp;gt;&amp;gt;([
        pad(32, "bboard:pk:"),
        sequence,
        sk
    ]);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;"bboard:pk:"&lt;/code&gt; prefix ensures public keys derived here can't be confused with hashes from any other part of the system — including other contracts that might use the same &lt;code&gt;sk&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Combining all three: a governance contract
&lt;/h2&gt;

&lt;p&gt;The strongest contracts layer all three mechanisms. Here's a minimal DAO governance contract that uses nonces for proposal submission ordering, domain-separated nullifiers for anonymous voting, and explicit tag versioning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma language_version &amp;gt;= 0.20;
import CompactStandardLibrary;

// Phase state
export ledger proposalOpen: Boolean;
export ledger proposalId: Bytes&amp;lt;32&amp;gt;;

// Counter nonce: proposal submission is identity-bound and sequentially ordered
export ledger submitterNonces: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Uint&amp;lt;64&amp;gt;&amp;gt;;
export ledger proposalCount: Counter;

// Nullifier set: voting is anonymous — nullifiers prevent double-vote
export ledger voteNullifiers: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Boolean&amp;gt;;
export ledger yesVotes: Counter;
export ledger noVotes: Counter;

witness getVoterSecret(): Bytes&amp;lt;32&amp;gt;;

// Domain-separated helper — "dao:vote:v1" isolates vote hashes from all other ops
circuit computeVoteNullifier(
    secret: Bytes&amp;lt;32&amp;gt;,
    proposal: Bytes&amp;lt;32&amp;gt;
): Bytes&amp;lt;32&amp;gt; {
    return persistentHash&amp;lt;Vector&amp;lt;3, Bytes&amp;lt;32&amp;gt;&amp;gt;&amp;gt;([
        pad(32, "dao:vote:v1"),
        secret,
        proposal
    ]);
}

// Submit a proposal — identity-bound, nonce-protected
export circuit submitProposal(
    submitterKey: Bytes&amp;lt;32&amp;gt;,
    nonce: Uint&amp;lt;64&amp;gt;,
    nextNonce: Uint&amp;lt;64&amp;gt;,
    content: Bytes&amp;lt;32&amp;gt;
): [] {
    assert(!proposalOpen, "Proposal already open");

    // Counter nonce: verify and advance
    if (!submitterNonces.member(disclose(submitterKey))) {
        assert(disclose(nonce) == 0, "First submission must use nonce 0");
    } else {
        assert(
            disclose(nonce) == submitterNonces.lookup(disclose(submitterKey)),
            "Invalid nonce"
        );
    }
    assert(disclose(nextNonce) &amp;gt; disclose(nonce), "nextNonce must increase");
    submitterNonces.insert(disclose(submitterKey), disclose(nextNonce));

    proposalId = disclose(content);
    proposalOpen = disclose(true);
    proposalCount.increment(1);
}

// Cast a vote — anonymous, nullifier-protected, domain-separated
export circuit castVote(vote: Uint&amp;lt;8&amp;gt;): [] {
    assert(proposalOpen, "No open proposal");

    const secret = getVoterSecret();
    const nullifier = computeVoteNullifier(secret, proposalId);

    // Replay prevention: nullifier check before side effects
    assert(!voteNullifiers.member(disclose(nullifier)), "Already voted");
    voteNullifiers.insert(disclose(nullifier), disclose(true));

    if (disclose(vote) == 1) {
        yesVotes.increment(1);
    } else {
        noVotes.increment(1);
    }
}

export circuit closeProposal(): [] {
    assert(proposalOpen, "No open proposal");
    proposalOpen = disclose(false);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This contract uses nonces for the submitter (identity matters, ordering matters) and nullifiers for voters (identity is private, concurrency is expected). The &lt;code&gt;"dao:vote:v1"&lt;/code&gt; tag ensures vote hashes can never collide with any other operation in a larger system, and the version suffix leaves room for future upgrades.&lt;/p&gt;




&lt;h2&gt;
  
  
  When to use which
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Registered users, strict sequential ordering needed&lt;/td&gt;
&lt;td&gt;Counter nonces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anonymous participation, high concurrency&lt;/td&gt;
&lt;td&gt;Set-based nullifiers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple distinct operations in one contract&lt;/td&gt;
&lt;td&gt;Domain separation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-contract system sharing user secrets&lt;/td&gt;
&lt;td&gt;Context-scoped nullifiers + domain separation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time-bound campaigns (voting, airdrops)&lt;/td&gt;
&lt;td&gt;Nullifiers with campaign ID in context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contract upgrade path needed&lt;/td&gt;
&lt;td&gt;Versioned domain tags (&lt;code&gt;v1&lt;/code&gt;, &lt;code&gt;v2&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Counter nonces and nullifiers both prevent operation replay, but they trade off differently: nonces are sequential and identity-revealing; nullifiers allow concurrency and preserve privacy. Domain separation is not an alternative to either — it's a required complement whenever a contract has more than one circuit that hashes related data.&lt;/p&gt;

&lt;p&gt;The strongest contracts layer all three: domain-separated nullifiers for the core replay check, plus counter nonces for any ordered operations that require strict sequencing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Using &lt;code&gt;Set&amp;lt;T&amp;gt;&lt;/code&gt; — doesn't exist in Compact
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ Compile error — Compact has no Set type
export ledger spentNullifiers: Set&amp;lt;Bytes&amp;lt;32&amp;gt;&amp;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;// ✅ Correct — use Map&amp;lt;K, Boolean&amp;gt; as a set
export ledger spentNullifiers: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Boolean&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. &lt;code&gt;Map.lookup()&lt;/code&gt; without &lt;code&gt;Map.member()&lt;/code&gt; check — panics on missing key
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ Panics at proof generation if userKey has never been inserted
const current = userNonces.lookup(disclose(userKey));
&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;// ✅ Always check member() first
if (userNonces.member(disclose(userKey))) {
    const current = userNonces.lookup(disclose(userKey));
    assert(disclose(nonce) == current, "Invalid nonce");
} else {
    assert(disclose(nonce) == 0, "First use must start at nonce 0");
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. &lt;code&gt;Uint&amp;lt;64&amp;gt;&lt;/code&gt; arithmetic in-circuit produces a wider range type
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ Type error: Uint&amp;lt;64&amp;gt; + 1 produces Uint&amp;lt;1..2^64&amp;gt;, not Uint&amp;lt;64&amp;gt;
userNonces.insert(disclose(userKey), disclose(current + 1));
&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;expected right-hand side to have type Uint&amp;lt;64&amp;gt;
but received Uint&amp;lt;1..18446744073709551616&amp;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;// ✅ Pass nextNonce from off-chain TypeScript, verify &amp;gt; current in-circuit
userNonces.insert(disclose(userKey), disclose(nextNonce));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Missing &lt;code&gt;disclose()&lt;/code&gt; on exported parameters in comparisons
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ Compiler flags the comparison — may produce disclosure error
assert(nonce == userNonces.lookup(disclose(userKey)), "Invalid nonce");
&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;// ✅ Exported parameters need disclose() before ledger comparisons
assert(disclose(nonce) == userNonces.lookup(disclose(userKey)), "Invalid nonce");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compact compiler error: &lt;code&gt;potential witness-value disclosure must be declared but is not — performing this ledger operation might disclose the boolean value of the result of a comparison involving the witness value&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Nullifiers without context — same nullifier across deployments
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ Same secret produces same nullifier in every contract — cross-contract replay risk
const nullifier = persistentCommit&amp;lt;Bytes&amp;lt;32&amp;gt;&amp;gt;(secret, pad(32, "no-scope"));
&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;// ✅ Include contract-specific context in the opening
const nullifier = persistentCommit&amp;lt;Bytes&amp;lt;32&amp;gt;&amp;gt;(secret, disclose(campaignContext));
// where campaignContext includes contract address or unique deployment ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. Domain tags without version — upgrade breaks nullifier isolation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ No version — upgrading the contract logic shares the hash space with v1
pad(32, "gov:vote")
&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;// ✅ Versioned — v2 deployments have isolated nullifier sets from v1
pad(32, "gov:vote:v1")
pad(32, "gov:vote:v2")  // for the upgraded contract
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  7. Recording nullifier after side effects
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ❌ Side effects before nullifier record — logically backwards
totalClaims.increment(1);
spentNullifiers.insert(disclose(nullifier), disclose(true));
&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;// ✅ Record nullifier first, then side effects
spentNullifiers.insert(disclose(nullifier), disclose(true));
totalClaims.increment(1);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compact circuits are atomic, so this doesn't affect transaction safety. But it protects you during audits and contract evolution, and is the defensively correct pattern.&lt;/p&gt;




&lt;h2&gt;
  
  
  Compiler-verified source
&lt;/h2&gt;

&lt;p&gt;All three contracts in this article — &lt;code&gt;counter-nonce.compact&lt;/code&gt;, &lt;code&gt;nullifier.compact&lt;/code&gt;, and &lt;code&gt;domain-separation.compact&lt;/code&gt; — compile against the latest Compact compiler. The full source and CI run are at:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/IamHarrie-Labs/compact-replay-prevention-guide/actions/runs/25690416194" rel="noopener noreferrer"&gt;https://github.com/IamHarrie-Labs/compact-replay-prevention-guide/actions/runs/25690416194&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.midnight.network/develop/reference/compact/lang-ref" rel="noopener noreferrer"&gt;Compact Language Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.midnight.network/compact/standard-library/exports" rel="noopener noreferrer"&gt;Standard Library: &lt;code&gt;persistentHash&lt;/code&gt;, &lt;code&gt;persistentCommit&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.midnight.network/tutorials/bboard/smart-contract" rel="noopener noreferrer"&gt;Bulletin Board Tutorial&lt;/a&gt; — canonical domain separation example (&lt;code&gt;"bboard:pk:"&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forum.midnight.network/" rel="noopener noreferrer"&gt;Midnight Developer Forum&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>midnightfordevs</category>
      <category>blockchain</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Understanding Wallet Sync in the Midnight SDK: Why Your Deploy Fails Before It Starts</title>
      <dc:creator>Harrie</dc:creator>
      <pubDate>Mon, 11 May 2026 18:29:32 +0000</pubDate>
      <link>https://dev.to/iamharrie/understanding-wallet-sync-in-the-midnight-sdk-why-your-deploy-fails-before-it-starts-1hpp</link>
      <guid>https://dev.to/iamharrie/understanding-wallet-sync-in-the-midnight-sdk-why-your-deploy-fails-before-it-starts-1hpp</guid>
      <description>&lt;p&gt;You call &lt;code&gt;balanceUnboundTransaction&lt;/code&gt;. It throws — or worse, it succeeds silently with wrong token balances, and the transaction fails somewhere downstream with a message that has nothing to do with sync. You check the obvious things. Funds are in the wallet. The contract compiled. The proof server is running. Nothing looks wrong.&lt;/p&gt;

&lt;p&gt;The problem is that your wallet hasn't finished scanning the chain, and &lt;code&gt;balanceUnboundTransaction&lt;/code&gt; built a transaction from an incomplete local database. This is probably the most common early failure in Midnight development. It's almost always invisible until you've seen it once.&lt;/p&gt;




&lt;h2&gt;
  
  
  The mental model shift
&lt;/h2&gt;

&lt;p&gt;Most developers approach a crypto wallet like a stateless lookup tool — you call it, it queries the network, you get a live answer. Midnight's wallet doesn't work that way.&lt;/p&gt;

&lt;p&gt;The Midnight wallet is a local database. Before it can do anything useful, it has to scan the blockchain from its last known position (or from genesis for a fresh wallet) to discover which UTXOs and notes belong to it. Until that scan completes, the wallet has an incomplete, potentially stale view of its own balances.&lt;/p&gt;

&lt;p&gt;Shielded transactions make this more expensive than other chains. Every shielded note on Midnight is encrypted. The wallet has no shortcut — it has to attempt to decrypt every note it encounters to discover which ones it owns. On a busy testnet, that's a lot of blocks.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;balanceUnboundTransaction&lt;/code&gt; pulls from this local database. Pass it before the scan completes and it builds a transaction from whatever partial state it has. On a fresh wallet with nothing scanned, it finds no UTXOs and errors. After partial sync, it might find stale UTXOs, build a valid-looking transaction, and have that transaction rejected at submission time. Neither failure is easy to debug from the error message alone.&lt;/p&gt;




&lt;h2&gt;
  
  
  The three sub-wallets
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;WalletFacade&lt;/code&gt; wraps three independent sub-wallets, each scanning for different things and completing at different speeds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shielded wallet&lt;/strong&gt; manages private NIGHT tokens via Zswap notes. Every block it encounters requires trial decryption against your secret keys. This is the slowest of the three — on a cold testnet start with hundreds of blocks to scan, expect 30–60 seconds. The state path is &lt;code&gt;state.shielded.state.progress&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unshielded wallet&lt;/strong&gt; handles transparent UTXOs, visible on-chain without ZK protection. Scanning is faster because it can filter by address without decryption. The state path is &lt;code&gt;state.unshielded.progress&lt;/code&gt; — note the different nesting from shielded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DUST wallet&lt;/strong&gt; manages tDUST fee tokens via UTXO aggregation. On an active chain it's near-instant. On an idle chain (a local devnet with no recent transactions), it hits a documented bug. The state path is &lt;code&gt;state.dust.state.progress&lt;/code&gt; — back to the extra &lt;code&gt;.state&lt;/code&gt; level, like shielded.&lt;/p&gt;

&lt;p&gt;All three need to complete before &lt;code&gt;balanceUnboundTransaction&lt;/code&gt; can do its job. &lt;code&gt;balanceUnboundTransaction&lt;/code&gt; needs shielded keys to find your private notes, and the DUST wallet to cover fees. Missing either one and it either errors or produces bad output.&lt;/p&gt;




&lt;h2&gt;
  
  
  The state shape: what &lt;code&gt;facade.state()&lt;/code&gt; actually emits
&lt;/h2&gt;

&lt;p&gt;This is the part of the API that trips people up most — the three sub-wallets have inconsistent state shapes:&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;// These three paths are NOT symmetric&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;shielded&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;progress&lt;/span&gt;    &lt;span class="c1"&gt;// shielded: extra .state level&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;unshielded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt;        &lt;span class="c1"&gt;// unshielded: flatter&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;dust&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;progress&lt;/span&gt;        &lt;span class="c1"&gt;// dust: extra .state level, like shielded&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you try to check sync completion by calling &lt;code&gt;.isStrictlyComplete()&lt;/code&gt; directly on the wrong path, you get &lt;code&gt;undefined&lt;/code&gt;, which is falsy, which means your sync check always fails — but silently.&lt;/p&gt;

&lt;p&gt;The official Midnight CLI code uses a defensive wrapper for exactly this reason:&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;isProgressStrictlyComplete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&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="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;progress&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;progress&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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;candidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;progress&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isStrictlyComplete&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;unknown&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isStrictlyComplete&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isStrictlyComplete&lt;/span&gt; &lt;span class="k"&gt;as &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;boolean&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 handles SDK versions where &lt;code&gt;isStrictlyComplete&lt;/code&gt; might not exist on the progress object, and accounts for return values that are &lt;code&gt;boolean | undefined&lt;/code&gt; rather than a clean boolean. Call it with each sub-wallet's progress field and you get a safe, consistent answer.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;isSynced&lt;/code&gt; vs &lt;code&gt;isStrictlyComplete()&lt;/code&gt; — they're not the same thing
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;WalletFacade&lt;/code&gt; exposes a top-level &lt;code&gt;isSynced&lt;/code&gt; flag. It looks like the right thing to check, and it's tempting to use it. The problem: it's a convenience property that can return &lt;code&gt;true&lt;/code&gt; while the DUST wallet's &lt;code&gt;isStrictlyComplete()&lt;/code&gt; is still &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you gate your transaction on &lt;code&gt;isSynced&lt;/code&gt; alone, you might proceed with a wallet that can't cover fees. The transaction builds, passes &lt;code&gt;balanceUnboundTransaction&lt;/code&gt;, and fails at submission with an opaque message about DUST.&lt;/p&gt;

&lt;p&gt;Midnight's own official sync code doesn't use &lt;code&gt;isSynced&lt;/code&gt;. It checks each sub-wallet individually:&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;Rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&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;FacadeState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;isProgressStrictlyComplete&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;shielded&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;progress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nf"&gt;isProgressStrictlyComplete&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;dust&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;progress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nf"&gt;isProgressStrictlyComplete&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;unshielded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;progress&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;If you want production reliability, follow the official code, not the convenience flag.&lt;/p&gt;




&lt;h2&gt;
  
  
  The correct sync pattern
&lt;/h2&gt;

&lt;p&gt;Here's &lt;code&gt;syncWallet&lt;/code&gt; from Midnight's Bulletin Board CLI tutorial — the canonical implementation straight from the docs:&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;syncWallet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WalletFacade&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;throttleTime&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;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&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;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Syncing wallet...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;firstValueFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;state&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;Rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tap&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FacadeState&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shieldedSynced&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;isProgressStrictlyComplete&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;shielded&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;progress&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;unshieldedSynced&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;isProgressStrictlyComplete&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;unshielded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;progress&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;dustSynced&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;isProgressStrictlyComplete&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;dust&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;progress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="s2"&gt;`Sync progress: shielded=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;shieldedSynced&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, unshielded=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;unshieldedSynced&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, dust=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dustSynced&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="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="nx"&gt;Rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;throttleTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;throttleTime&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;Rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FacadeState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
          &lt;span class="nf"&gt;isProgressStrictlyComplete&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;shielded&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;progress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
          &lt;span class="nf"&gt;isProgressStrictlyComplete&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;dust&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;progress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
          &lt;span class="nf"&gt;isProgressStrictlyComplete&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;unshielded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;Rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tap&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sync complete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
      &lt;span class="nx"&gt;Rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;each&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;throwError&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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="s2"&gt;`Wallet sync timeout after &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms`&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="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;Rx.throttleTime(2_000)&lt;/code&gt; is there because &lt;code&gt;wallet.state()&lt;/code&gt; emits on every block — potentially multiple times per second during active sync. Without throttling, the filter runs on every emission and any side effects (logging, UI updates) fire hundreds of times per sync. The &lt;code&gt;tap&lt;/code&gt; before &lt;code&gt;throttleTime&lt;/code&gt; still logs every emission at DEBUG level, but the filter only evaluates every 2 seconds.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Rx.timeout({ each: timeout, ... })&lt;/code&gt; — the &lt;code&gt;each&lt;/code&gt; key applies per-emission gap, not total elapsed time. If the wallet emits on schedule (every 2 seconds from throttling), &lt;code&gt;each: 90_000&lt;/code&gt; only fires if 90 seconds pass with &lt;em&gt;no emission at all&lt;/em&gt; — meaning the wallet has gone completely silent. That's the right semantics: catch stuck wallets, not just slow ones.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Rx.firstValueFrom&lt;/code&gt; converts the observable into a Promise that resolves as soon as the filter passes. Easy to await in async code without managing subscriptions manually.&lt;/p&gt;




&lt;h2&gt;
  
  
  The DUST wallet bug on idle chains
&lt;/h2&gt;

&lt;p&gt;On chains with little or no recent transaction activity — a local devnet you just started, or an integration test environment — the DUST wallet's &lt;code&gt;isStrictlyComplete()&lt;/code&gt; never returns &lt;code&gt;true&lt;/code&gt;. The progress tracker doesn't see any DUST consolidation events, so it never records completion. &lt;code&gt;syncWallet&lt;/code&gt; hangs until the timeout fires.&lt;/p&gt;

&lt;p&gt;The workaround is to skip the DUST strict-completion requirement in development environments:&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;syncWalletWithFallback&lt;/span&gt; &lt;span class="o"&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;wallet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WalletFacade&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isDev&lt;/span&gt; &lt;span class="o"&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;timeoutMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;firstValueFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;state&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;Rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;throttleTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;Rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FacadeState&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shieldedReady&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;isProgressStrictlyComplete&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;shielded&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;progress&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;unshieldedReady&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;isProgressStrictlyComplete&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;unshielded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;progress&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;dustReady&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;isProgressStrictlyComplete&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;dust&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;progress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// On idle devnets, DUST never completes — skip that check in dev&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;shieldedReady&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;unshieldedReady&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dustReady&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;isDev&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="nx"&gt;Rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;each&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;throwError&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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="s2"&gt;`Sync timeout after &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms`&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="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;Don't use this shortcut in production. If DUST sync is incomplete on mainnet or testnet, fee payments will fail.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;balanceUnboundTransaction&lt;/code&gt; — the full picture
&lt;/h2&gt;

&lt;p&gt;Every competitor article treats &lt;code&gt;balanceUnboundTransaction&lt;/code&gt; as a one-line call. The actual signature is:&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;recipe&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;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;balanceUnboundTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                              &lt;span class="c1"&gt;// the unbound transaction from your circuit&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;shieldedSecretKeys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;zswapSecretKeys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// Zswap keys (plural — an array)&lt;/span&gt;
    &lt;span class="na"&gt;dustSecretKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dustSecretKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;// DUST key (singular)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ttl&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;                                         &lt;span class="c1"&gt;// time-to-live for the transaction&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two separate key types: &lt;code&gt;shieldedSecretKeys&lt;/code&gt; is an array of Zswap keys that unlock your shielded notes, and &lt;code&gt;dustSecretKey&lt;/code&gt; is a single key for the DUST fee wallet. If you pass the wrong key type or omit one, the call fails — or worse, builds a transaction that covers the wrong balance. This is separate from sync, but it's the other common reason this call fails.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;balanceUnboundTransaction&lt;/code&gt; also doesn't submit the transaction. It returns a recipe. You need two more steps:&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;// Step 1: Balance the unbound transaction&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;recipe&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;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;balanceUnboundTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ttl&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Step 2: Finalize the recipe into a submittable transaction&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;finalized&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;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finalizeRecipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Step 3: Submit&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;txHash&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;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submitTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;finalized&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This three-step flow matters because &lt;code&gt;finalizeRecipe&lt;/code&gt; is where ZK proofs get generated. If you're debugging a failure and it happens in &lt;code&gt;finalizeRecipe&lt;/code&gt; rather than &lt;code&gt;balanceUnboundTransaction&lt;/code&gt;, the problem is in proof generation, not sync.&lt;/p&gt;




&lt;h2&gt;
  
  
  The full startup sequence
&lt;/h2&gt;

&lt;p&gt;From Midnight's own CLI tutorial, the full sequence looks like this:&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;// 1. Create the wallet provider and get the facade&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;walletProvider&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;MidnightWalletProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;envConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;seed&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;wallet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WalletFacade&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;walletProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Start the wallet (begins network connections and scanning)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;walletProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// 3. If this is a first-time setup: wait for unshielded funds and generate DUST&lt;/span&gt;
&lt;span class="c1"&gt;//    Skip this if the wallet already has DUST from a previous run.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unshieldedState&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;waitForUnshieldedFunds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;envConfig&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;generateDust&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unshieldedState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 4. Wait for full sync before any transactions&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;syncWallet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 5. Now safe to call balanceUnboundTransaction&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Steps 3 is first-run setup: &lt;code&gt;generateDust&lt;/code&gt; creates the initial tDUST that fees will be paid from. On subsequent runs with a funded wallet, skip it. The key point is that step 4 — &lt;code&gt;syncWallet&lt;/code&gt; — must always happen before step 5. The &lt;code&gt;start()&lt;/code&gt; call in step 2 begins scanning, but does not wait for completion. There's always a gap.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three failure modes
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;balanceUnboundTransaction&lt;/code&gt; runs before sync completes, the failure depends on how far along sync is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not started&lt;/strong&gt;: The wallet has no UTXOs indexed at all. You'll get an immediate error about insufficient balance or missing UTXOs. Annoying but at least clear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Partial sync&lt;/strong&gt;: The wallet has some UTXOs but not all. &lt;code&gt;balanceUnboundTransaction&lt;/code&gt; picks the UTXOs it knows about, which may be stale or already spent. The call succeeds, but the transaction gets rejected at submission time with a message about invalid inputs or double-spend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shielded-only sync&lt;/strong&gt;: Shielded wallet is complete, unshielded and DUST are not. Transaction builds with correct private balance but fails on fee payment with "insufficient DUST balance" — even if the faucet topped you up.&lt;/p&gt;

&lt;p&gt;The second failure mode is the worst to diagnose. You have a finalized transaction, you submitted it, and it fails with a message that doesn't mention sync at all.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production patterns
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Always gate transactions on sync.&lt;/strong&gt; Even after the initial startup sync, wallets can fall behind if the node connection drops or the chain catches up. Check before every &lt;code&gt;balanceUnboundTransaction&lt;/code&gt;, not just at startup:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;safeBalance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WalletFacade&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UnboundTransaction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WalletKeys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Recipe&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;// Confirm sync first, re-sync if needed&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;firstValueFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;state&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
  &lt;span class="k"&gt;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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isSynced&lt;/span&gt;&lt;span class="p"&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;syncWallet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wallet&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="nx"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;balanceUnboundTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;ttlOneHour&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;strong&gt;Re-sync on transaction failure.&lt;/strong&gt; If submission fails with UTXO-related errors, the local database may have fallen behind. Re-sync before retrying:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;submitWithRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WalletFacade&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UnboundTransaction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WalletKeys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;maxRetries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&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;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempt&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="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="o"&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;try&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;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;syncWallet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&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;recipe&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;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;balanceUnboundTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;ttlOneHour&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;finalized&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;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finalizeRecipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submitTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;finalized&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;err&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;attempt&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Attempt &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;attempt&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="s2"&gt; failed: &lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&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="s2"&gt;. Re-syncing...`&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;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="s1"&gt;unreachable&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Monitor sync state continuously.&lt;/strong&gt; In a long-running service, subscribe to wallet state and alert when the wallet desyncs:&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;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;state&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&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="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="o"&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;isSynced&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&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 lost sync — queuing transactions until recovery&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="c1"&gt;// block new transaction requests, drain the in-flight queue&lt;/span&gt;
    &lt;span class="p"&gt;}&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&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 state subscription error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&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;h2&gt;
  
  
  Common pitfalls: wrong and right
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Wrong&lt;/strong&gt; — calling &lt;code&gt;balanceUnboundTransaction&lt;/code&gt; immediately after &lt;code&gt;start()&lt;/code&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;walletProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// ❌ start() doesn't wait for sync&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;recipe&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;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;balanceUnboundTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ttl&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;Right&lt;/strong&gt; — wait for sync before any transaction work:&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;walletProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&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;syncWallet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ✅ blocks until all three sub-wallets are complete&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;recipe&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;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;balanceUnboundTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ttl&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;Wrong&lt;/strong&gt; — checking &lt;code&gt;isSynced&lt;/code&gt; and trusting it for DUST:&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;state&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;Rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;firstValueFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;state&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="k"&gt;if &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;isSynced&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ❌ isSynced can be true while DUST's isStrictlyComplete() is false&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;balanceUnboundTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ttl&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Right&lt;/strong&gt; — check each sub-wallet individually:&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;state&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;Rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;firstValueFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;state&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;take&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allSynced&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nf"&gt;isProgressStrictlyComplete&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;shielded&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;progress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nf"&gt;isProgressStrictlyComplete&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;unshielded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nf"&gt;isProgressStrictlyComplete&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;dust&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;progress&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;allSynced&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;balanceUnboundTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ttl&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Wrong&lt;/strong&gt; — calling &lt;code&gt;.isStrictlyComplete()&lt;/code&gt; directly without the defensive wrapper:&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;// ❌ isStrictlyComplete can be undefined in some SDK versions&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dustReady&lt;/span&gt; &lt;span class="o"&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;dust&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;progress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isStrictlyComplete&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;Right&lt;/strong&gt; — use the defensive helper:&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;// ✅ handles undefined, null, and function-not-present cases&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dustReady&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;isProgressStrictlyComplete&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;dust&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;progress&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;Wrong&lt;/strong&gt; — treating &lt;code&gt;balanceUnboundTransaction&lt;/code&gt; as a submit call:&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;// ❌ this only balances the transaction; it doesn't submit it&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;balanceUnboundTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ttl&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// nothing submitted, no error, silently did nothing useful&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Right&lt;/strong&gt; — use the full three-step flow:&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;recipe&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;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;balanceUnboundTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ttl&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// ✅ balance&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;finalized&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;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finalizeRecipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                     &lt;span class="c1"&gt;// ✅ prove&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;txHash&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;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submitTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;finalized&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                  &lt;span class="c1"&gt;// ✅ submit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Quick reference
&lt;/h2&gt;

&lt;p&gt;Midnight's wallet is a local database rebuilt by scanning the chain. &lt;code&gt;balanceUnboundTransaction&lt;/code&gt; reads that database, so calling it before sync means building transactions from incomplete data.&lt;/p&gt;

&lt;p&gt;The three sub-wallets sync independently and have inconsistent state shapes — check each one explicitly with the &lt;code&gt;isProgressStrictlyComplete&lt;/code&gt; helper rather than trusting the top-level &lt;code&gt;isSynced&lt;/code&gt; flag.&lt;/p&gt;

&lt;p&gt;The sequence, from Midnight's own CLI code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;await walletProvider.start()&lt;/code&gt; — begins scanning, does not wait for it&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;await syncWallet(logger, wallet)&lt;/code&gt; — blocks until all three sub-wallets are strictly complete&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;balanceUnboundTransaction&lt;/code&gt; → &lt;code&gt;finalizeRecipe&lt;/code&gt; → &lt;code&gt;submitTransaction&lt;/code&gt; — three separate steps&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On idle devnets, DUST sync will never complete — use the fallback that skips it in dev. In production, always require all three.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;TypeScript patterns in this article are derived from Midnight's official Bulletin Board CLI tutorial. Wallet SDK API based on `@midnight-ntwrk/midnight-js-&lt;/em&gt;` packages.*&lt;/p&gt;

</description>
      <category>midnightfordevs</category>
      <category>blockchain</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building a Commit/Reveal Voting System in Compact</title>
      <dc:creator>Harrie</dc:creator>
      <pubDate>Mon, 11 May 2026 18:05:13 +0000</pubDate>
      <link>https://dev.to/iamharrie/building-a-commitreveal-voting-system-in-compact-5gb2</link>
      <guid>https://dev.to/iamharrie/building-a-commitreveal-voting-system-in-compact-5gb2</guid>
      <description>&lt;p&gt;On-chain voting has a timing problem. When votes are cast publicly, participants can watch early results and adjust their choices accordingly. In a close race, seeing 60% favor "yes" influences how undecided voters behave. In adversarial contexts, it's worse: coercers can observe and manipulate votes when choices are visible on-chain in real time.&lt;/p&gt;

&lt;p&gt;The commit/reveal pattern addresses this by splitting voting into two phases. During the commit phase, voters submit a cryptographic commitment to their vote — something that binds them to a choice without revealing it. Once the commit window closes, the reveal phase opens: voters prove their commitment matches a vote, the ZK circuit tallies without trusting the voter's self-reported value, and nobody can change their vote retroactively.&lt;/p&gt;

&lt;p&gt;Midnight's Compact makes this unusually clean. &lt;code&gt;persistentCommit&lt;/code&gt; gives a proper hiding commitment scheme (not just a hash). &lt;code&gt;HistoricMerkleTree&lt;/code&gt; makes concurrent voter registration race-condition-free. Witness functions keep all private key material off-chain while ZK proofs verify correctness without exposing secrets.&lt;/p&gt;

&lt;p&gt;This tutorial builds the full thing: commit and reveal circuits, a block-height phase state machine, Merkle-tree eligibility checks, domain-separated nullifiers, TypeScript witnesses, Vitest tests, and a minimal React frontend. All Compact code compiles against the latest compiler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; Midnight toolchain, basic Compact familiarity, TypeScript.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;persistentHash&lt;/code&gt; vs &lt;code&gt;persistentCommit&lt;/code&gt;: the core distinction
&lt;/h2&gt;

&lt;p&gt;This is the one thing to get right before writing any circuit. Get it wrong and vote privacy doesn't hold.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;persistentHash&amp;lt;T&amp;gt;(v: T): Bytes&amp;lt;32&amp;gt;&lt;/code&gt; is deterministic. Same input, same output, always. For a three-option vote (no / yes / abstain), that's a problem: an observer can crack any commitment in three tries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const hashNo      = persistentHash&amp;lt;Uint&amp;lt;8&amp;gt;&amp;gt;(0);
const hashYes     = persistentHash&amp;lt;Uint&amp;lt;8&amp;gt;&amp;gt;(1);
const hashAbstain = persistentHash&amp;lt;Uint&amp;lt;8&amp;gt;&amp;gt;(2);
// Three hashes covers the entire vote space
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A hash commitment is &lt;em&gt;binding&lt;/em&gt; (can't change your vote after submitting) but not &lt;em&gt;hiding&lt;/em&gt; (vote is discoverable by exhaustive check). For a small domain like vote choices, that's not a commitment scheme — it's just delayed disclosure.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;persistentCommit&amp;lt;T&amp;gt;(v: T, opening: Bytes&amp;lt;32&amp;gt;): Bytes&amp;lt;32&amp;gt;&lt;/code&gt; adds a 32-byte random blinding factor. An attacker would need to find an &lt;code&gt;opening&lt;/code&gt; such that &lt;code&gt;persistentCommit(guess, opening)&lt;/code&gt; matches the stored value. With 256 bits of randomness, that's not happening.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;persistentHash&lt;/code&gt; for nullifiers, where binding is what you need. Use &lt;code&gt;persistentCommit&lt;/code&gt; for vote commitments, where you need both.&lt;/p&gt;




&lt;h2&gt;
  
  
  Contract architecture
&lt;/h2&gt;

&lt;p&gt;The contract manages five things: phase state, voter eligibility, commitment storage, nullifier tracking, and vote tallies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma language_version &amp;gt;= 0.20;
import CompactStandardLibrary;

// 0 = COMMIT  — voters submit hashed votes
// 1 = REVEAL  — voters prove and tally
// 2 = CLOSED  — finalized, results available
export ledger phase: Uint&amp;lt;8&amp;gt;;
export ledger commitDeadline: Uint&amp;lt;64&amp;gt;;
export ledger revealDeadline: Uint&amp;lt;64&amp;gt;;

// HistoricMerkleTree keeps a full history of past roots.
// Voters registering concurrently won't invalidate each other's eligibility proofs.
export ledger voterTree: HistoricMerkleTree&amp;lt;16, Bytes&amp;lt;32&amp;gt;&amp;gt;;

// nullifierHash → voteCommitment
// Key:   persistentHash(voterSecret)        — ties commit to voter, doesn't reveal identity
// Value: persistentCommit(vote, blinder)    — hides vote until reveal
export ledger commitments: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Bytes&amp;lt;32&amp;gt;&amp;gt;;

// Spent nullifiers — prevents any voter from revealing twice
export ledger spentNullifiers: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Boolean&amp;gt;;

// Tallies and participation tracking
export ledger yesVotes: Counter;
export ledger noVotes: Counter;
export ledger abstainVotes: Counter;
export ledger totalCommits: Counter;
export ledger totalReveals: Counter;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why &lt;code&gt;HistoricMerkleTree&lt;/code&gt; and not &lt;code&gt;MerkleTree&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Easy to overlook, but it matters as soon as you have more than a handful of voters registering around the same time.&lt;/p&gt;

&lt;p&gt;A standard &lt;code&gt;MerkleTree&lt;/code&gt; proof is only valid against the &lt;em&gt;current&lt;/em&gt; root. Voter A generates their eligibility proof when the root is &lt;code&gt;R1&lt;/code&gt;. Before they submit, voter B registers, changing the root to &lt;code&gt;R2&lt;/code&gt;. Voter A's proof is now invalid. They regenerate it, but by then voter C has registered. Under any real load this becomes a liveness problem.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;HistoricMerkleTree&lt;/code&gt; keeps a history of every root the tree has ever had. A voter who generated their eligibility proof against root &lt;code&gt;R1&lt;/code&gt; can still submit their transaction even after other voters have registered and moved the root to &lt;code&gt;R2&lt;/code&gt; — the runtime accepts proofs against any root in the history. Concurrent registrations never invalidate outstanding proofs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Standard tree — proofs only valid against current root
export ledger voterTree: MerkleTree&amp;lt;16, Bytes&amp;lt;32&amp;gt;&amp;gt;;

// Historic tree — runtime keeps a history of all past roots
// Both use checkRoot() in circuits; HistoricMerkleTree allows the
// TypeScript client to regenerate proofs against any past root
export ledger voterTree: HistoricMerkleTree&amp;lt;16, Bytes&amp;lt;32&amp;gt;&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both &lt;code&gt;MerkleTree&lt;/code&gt; and &lt;code&gt;HistoricMerkleTree&lt;/code&gt; use &lt;code&gt;checkRoot()&lt;/code&gt; in Compact circuits — the method name is identical. The difference is entirely in the TypeScript layer: &lt;code&gt;HistoricMerkleTree&lt;/code&gt; exposes historical roots through the ledger API, letting client-side proof generation pick whichever past root is still valid for the voter's Merkle path. (Note: &lt;code&gt;checkRootInHistory&lt;/code&gt; is not a valid method — it causes a compile error. See pitfall #9.)&lt;/p&gt;




&lt;h2&gt;
  
  
  Witnesses
&lt;/h2&gt;

&lt;p&gt;Private inputs — voter secrets, vote values, blinding factors, Merkle paths — are supplied off-chain by the TypeScript client. They never appear on-chain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;witness getVoterSecret(): Bytes&amp;lt;32&amp;gt;;
witness getVote(): Uint&amp;lt;8&amp;gt;;       // 0 = NO, 1 = YES, 2 = ABSTAIN
witness getBlinder(): Bytes&amp;lt;32&amp;gt;;  // randomness for the vote commitment
witness getVoterPath(): MerkleTreePath&amp;lt;16, Bytes&amp;lt;32&amp;gt;&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Constructor
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;constructor(
    commitDeadlineBlock: Uint&amp;lt;64&amp;gt;,
    revealDeadlineBlock: Uint&amp;lt;64&amp;gt;
) {
    phase = disclose(0);
    commitDeadline = disclose(commitDeadlineBlock);
    revealDeadline = disclose(revealDeadlineBlock);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The constructor takes absolute block heights rather than durations. Compact's range-typed arithmetic means adding two &lt;code&gt;Uint&amp;lt;64&amp;gt;&lt;/code&gt; values produces a &lt;code&gt;Uint&amp;lt;0..2^65&amp;gt;&lt;/code&gt; — wider than &lt;code&gt;Uint&amp;lt;64&amp;gt;&lt;/code&gt; — which can't be assigned back without an explicit downcast. Doing the deadline arithmetic off-chain in TypeScript (where overflow is easier to handle) and passing the results in is cleaner.&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;// Off-chain: compute absolute deadlines before deploying&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentBlock&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;getBlockHeight&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;commitDeadline&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentBlock&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// 100 blocks for commit phase&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;revealDeadline&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;commitDeadline&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 100 blocks for reveal phase&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Phase transitions
&lt;/h2&gt;

&lt;p&gt;Phase transitions are permissionless. No admin key, no privileged actor. Anyone can advance the phase once the deadline passes. The current block height is passed as a public parameter, and the Midnight network verifies it matches the actual block height at transaction submission time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Advance from COMMIT to REVEAL once the commit window closes
export circuit openRevealPhase(currentBlock: Uint&amp;lt;64&amp;gt;): [] {
    assert(phase == 0, "Not in commit phase");
    assert(disclose(currentBlock) &amp;gt;= commitDeadline, "Commit period not over");
    phase = disclose(1);
}

// Seal the results once the reveal window closes
export circuit closeVoting(currentBlock: Uint&amp;lt;64&amp;gt;): [] {
    assert(phase == 1, "Not in reveal phase");
    assert(disclose(currentBlock) &amp;gt;= revealDeadline, "Reveal period not over");
    phase = disclose(2);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two circuits rather than one &lt;code&gt;advancePhase&lt;/code&gt; keeps each transition's preconditions explicit. No branching on phase state inside the circuit body.&lt;/p&gt;




&lt;h2&gt;
  
  
  Voter registration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export circuit registerVoter(voterKey: Bytes&amp;lt;32&amp;gt;): [] {
    assert(phase == 0, "Registration only during commit phase");
    voterTree.insert(disclose(voterKey));
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a production DAO, this would be gated by governance logic — a multisig approval, a token-weighted vote, or a KYC attestation. Here the only gate is the commit phase deadline. Once the commit window closes, the voter tree is locked.&lt;/p&gt;




&lt;h2&gt;
  
  
  The commit circuit
&lt;/h2&gt;

&lt;p&gt;During the commit phase, each voter submits two opaque 32-byte values: a &lt;code&gt;nullifierHash&lt;/code&gt; (&lt;code&gt;persistentHash(voterSecret)&lt;/code&gt;) that ties this commitment to the voter without revealing who they are, and a &lt;code&gt;voteCommitment&lt;/code&gt; (&lt;code&gt;persistentCommit(vote, blinder)&lt;/code&gt;) that hides the actual vote choice.&lt;/p&gt;

&lt;p&gt;Neither reveals the vote. The nullifier doesn't expose voter identity. The commitment can't be reversed without the blinding factor.&lt;/p&gt;

&lt;p&gt;The circuit also verifies voter eligibility via a Merkle path witness — the voter proves they're in the eligible voter set without disclosing which leaf is theirs.&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 commitVote(
    nullifierHash: Bytes&amp;lt;32&amp;gt;,
    voteCommitment: Bytes&amp;lt;32&amp;gt;
): [] {
    assert(phase == 0, "Not in commit phase");

    // Prove eligibility — the Merkle path stays private in the witness
    const path     = getVoterPath();
    const computed = merkleTreePathRoot&amp;lt;16, Bytes&amp;lt;32&amp;gt;&amp;gt;(path);
    assert(
        voterTree.checkRoot(disclose(computed)),
        "Voter not in eligibility tree"
    );

    // One commitment per voter
    assert(!commitments.member(disclose(nullifierHash)), "Already committed");

    commitments.insert(disclose(nullifierHash), disclose(voteCommitment));
    totalCommits.increment(1);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two &lt;code&gt;disclose()&lt;/code&gt; calls worth explaining. &lt;code&gt;checkRoot(disclose(computed))&lt;/code&gt; — &lt;code&gt;computed&lt;/code&gt; comes from a private witness path, so passing it into a ledger method requires explicit disclosure. This isn't about making the value "public" in any meaningful sense; it's telling the compiler you're aware a witness-derived value is entering a ledger operation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;commitments.member(disclose(nullifierHash))&lt;/code&gt; — even though &lt;code&gt;nullifierHash&lt;/code&gt; was supplied by the caller as a public circuit parameter, Compact treats exported parameters as potentially private until disclosed. &lt;code&gt;disclose()&lt;/code&gt; is required before any ledger operation, regardless of how the value got there.&lt;/p&gt;




&lt;h2&gt;
  
  
  The reveal circuit
&lt;/h2&gt;

&lt;p&gt;At reveal time, the vote becomes public — that's the point of the reveal phase. What stays private is &lt;em&gt;how&lt;/em&gt; the voter knows their vote matches the stored commitment.&lt;/p&gt;

&lt;p&gt;The ZK proof demonstrates: "I know a &lt;code&gt;(voterSecret, blinder)&lt;/code&gt; such that &lt;code&gt;persistentHash(voterSecret)&lt;/code&gt; maps to a stored commitment, and &lt;code&gt;persistentCommit(vote, blinder)&lt;/code&gt; matches the stored vote commitment for that key." Neither &lt;code&gt;voterSecret&lt;/code&gt; nor &lt;code&gt;blinder&lt;/code&gt; appears in the transaction. Only &lt;code&gt;vote&lt;/code&gt; is revealed.&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 revealVote(vote: Uint&amp;lt;8&amp;gt;): [] {
    assert(phase == 1, "Not in reveal phase");
    // disclose(vote) required: even in an assert, the compiler flags comparisons
    // on exported parameters that could branch circuit execution differently
    assert(disclose(vote) &amp;lt; 3, "Invalid vote: must be 0 (NO), 1 (YES), or 2 (ABSTAIN)");

    // Private inputs — supplied by TypeScript witness, never on-chain
    const secret  = getVoterSecret();
    const blinder = getBlinder();

    // Derive nullifier hash from private secret
    const nullifierHash = persistentHash&amp;lt;Bytes&amp;lt;32&amp;gt;&amp;gt;(secret);

    // Reconstruct commitment from the revealed vote and private blinder
    const derivedCommitment = persistentCommit&amp;lt;Uint&amp;lt;8&amp;gt;&amp;gt;(disclose(vote), blinder);

    // Verify a commitment exists for this voter's nullifier
    assert(
        commitments.member(disclose(nullifierHash)),
        "No commitment found for this voter"
    );

    // Verify the vote matches what was committed
    assert(
        derivedCommitment == commitments.lookup(disclose(nullifierHash)),
        "Vote commitment mismatch — wrong vote, secret, or blinder"
    );

    // Mark nullifier spent — prevents the same voter from revealing twice
    assert(!spentNullifiers.member(disclose(nullifierHash)), "Vote already revealed");
    spentNullifiers.insert(disclose(nullifierHash), disclose(true));

    // Tally — disclose(vote) is required in if/else conditions:
    // branching on an exported parameter and executing different ledger
    // operations per branch discloses which branch was taken.
    // Compact requires explicit disclose() to acknowledge this is intentional.
    if (disclose(vote) == 0) { noVotes.increment(1); }
    else if (disclose(vote) == 1) { yesVotes.increment(1); }
    else { abstainVotes.increment(1); }

    totalReveals.increment(1);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;assert(disclose(vote) &amp;lt; 3, ...)&lt;/code&gt; at the top catches invalid vote values early — they'd fail the commitment check anyway, but this gives a cleaner error message. And yes, &lt;code&gt;disclose()&lt;/code&gt; is required in the assert too. See pitfall #10.&lt;/p&gt;




&lt;h2&gt;
  
  
  Domain-separated nullifiers
&lt;/h2&gt;

&lt;p&gt;There's a quiet privacy leak in &lt;code&gt;nullifierHash = persistentHash(voterSecret)&lt;/code&gt;: &lt;code&gt;persistentHash&lt;/code&gt; is deterministic, so a voter using the same secret across multiple proposals produces identical nullifier hashes in every contract. Anyone watching the chain can see "this nullifier appears in proposals A, B, and C — same voter." Not ideal for a privacy-preserving system.&lt;/p&gt;

&lt;p&gt;The fix is domain separation: mix a unique proposal identifier into the nullifier before hashing. Compact doesn't expose arbitrary byte concatenation in circuits, so this is cleanest in the TypeScript layer.&lt;/p&gt;

&lt;p&gt;First, store the proposal ID as a sealed ledger field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sealed ledger proposalId: Bytes&amp;lt;32&amp;gt;;

// Note: in practice, pass absolute block heights (not durations) to avoid
// the Uint&amp;lt;64&amp;gt; arithmetic range issue described in pitfall #8.
constructor(
    id: Bytes&amp;lt;32&amp;gt;,
    commitDeadlineBlock: Uint&amp;lt;64&amp;gt;,
    revealDeadlineBlock: Uint&amp;lt;64&amp;gt;
) {
    proposalId     = disclose(id);
    phase          = disclose(0);
    commitDeadline = disclose(commitDeadlineBlock);
    revealDeadline = disclose(revealDeadlineBlock);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then derive the nullifier off-chain with the proposal ID mixed in:&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;function&lt;/span&gt; &lt;span class="nf"&gt;computeNullifierHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;voterSecret&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="nx"&gt;proposalId&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="nb"&gt;Uint8Array&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// XOR the secret with the proposal ID for domain separation&lt;/span&gt;
    &lt;span class="c1"&gt;// In production: use a proper KDF (HKDF or similar)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;combined&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;Uint8Array&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="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&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="nx"&gt;i&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="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&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;combined&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;voterSecret&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="nx"&gt;proposalId&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&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="nf"&gt;persistentHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;combined&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;Each contract instance has a unique &lt;code&gt;proposalId&lt;/code&gt;. Voters using the same secret across proposals produce different nullifier hashes — no cross-proposal correlation.&lt;/p&gt;




&lt;h2&gt;
  
  
  TypeScript integration
&lt;/h2&gt;

&lt;p&gt;The witness implementations supply private state during proof generation:&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="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;WitnessContext&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-ntwrk/compact-runtime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;VotePrivateState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;voterSecret&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="nl"&gt;vote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// 0 | 1 | 2&lt;/span&gt;
    &lt;span class="nl"&gt;blinder&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="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="na"&gt;getVoterSecret&lt;/span&gt;&lt;span class="p"&gt;:&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;VotePrivateState&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="o"&gt;=&amp;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;privateState&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;privateState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;voterSecret&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="na"&gt;getVote&lt;/span&gt;&lt;span class="p"&gt;:&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;VotePrivateState&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="o"&gt;=&amp;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;privateState&lt;/span&gt;&lt;span class="p"&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;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="nx"&gt;vote&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="na"&gt;getBlinder&lt;/span&gt;&lt;span class="p"&gt;:&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;VotePrivateState&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="o"&gt;=&amp;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;privateState&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;privateState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blinder&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="na"&gt;getVoterPath&lt;/span&gt;&lt;span class="p"&gt;:&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;VotePrivateState&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="o"&gt;=&amp;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;voterKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deriveVoterKey&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;privateState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;voterSecret&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;path&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;ledger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;voterTree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findPathForLeaf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;voterKey&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;path&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="s1"&gt;Voter not found in eligibility tree&lt;/span&gt;&lt;span class="dl"&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="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="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&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;A &lt;code&gt;VotingAPI&lt;/code&gt; class wraps the circuit calls and manages private state:&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;class&lt;/span&gt; &lt;span class="nc"&gt;VotingAPI&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Contract&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;witnesses&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;private&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;CircuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;proposalId&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="p"&gt;{}&lt;/span&gt;

    &lt;span class="nf"&gt;registerVoter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;voterKey&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="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="k"&gt;this&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="o"&gt;=&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerVoter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&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;voterKey&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;commitVote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;voterSecret&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="nx"&gt;vote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1&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="nx"&gt;blinder&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;nullifierHash&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computeNullifierHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;voterSecret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;proposalId&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;voteCommitment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computeVoteCommitment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;blinder&lt;/span&gt;&lt;span class="p"&gt;);&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="k"&gt;this&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="o"&gt;=&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commitVote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="k"&gt;this&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;nullifierHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nx"&gt;voteCommitment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="c1"&gt;// Store locally — needed for the reveal transaction&lt;/span&gt;
        &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;voteSecret&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;voterSecret&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
        &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;voteBlinder&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blinder&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
        &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;voteChoice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vote&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;revealVote&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;secret&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;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;voteSecret&lt;/span&gt;&lt;span class="dl"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blinder&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;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;voteBlinder&lt;/span&gt;&lt;span class="dl"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vote&lt;/span&gt;    &lt;span class="o"&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;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;voteChoice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&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="o"&gt;=&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revealVote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&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="nc"&gt;BigInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vote&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;advancePhase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentBlock&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="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="nf"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&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;currentQueryContext&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="k"&gt;if &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;phase&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&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;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="k"&gt;this&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="o"&gt;=&lt;/span&gt;
                &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openRevealPhase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&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;currentBlock&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&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;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="k"&gt;this&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="o"&gt;=&lt;/span&gt;
                &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closeVoting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&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;currentBlock&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;getResults&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="nf"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&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;currentQueryContext&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;phase&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="na"&gt;yes&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;yesVotes&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="na"&gt;no&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;noVotes&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="na"&gt;abstain&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;abstainVotes&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="na"&gt;totalCommits&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalCommits&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="na"&gt;totalReveals&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalReveals&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Tests
&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;beforeEach&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;vitest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;createConstructorContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;createCircuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;sampleContractAddress&lt;/span&gt;&lt;span class="p"&gt;,&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-ntwrk/compact-runtime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ledger&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;../managed/voting/contract/index.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;witnesses&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;./witnesses.js&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;proposalId&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;Uint8Array&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="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&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;makeSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&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="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&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;makeBlinder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&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="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seed&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&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;makeVoterKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&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="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seed&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&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;createSimulator&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;contract&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;Contract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;witnesses&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;constructorCtx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createConstructorContext&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;Uint8Array&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;currentPrivateState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentContractState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentZswapLocalState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;constructorCtx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="nx"&gt;n&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;circuitContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createCircuitContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;sampleContractAddress&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="nx"&gt;currentZswapLocalState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;currentContractState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;currentPrivateState&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="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;circuitContext&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;commit/reveal voting&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;accepts a valid commit then reveal&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSimulator&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;secret&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;makeSecret&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blinder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;makeBlinder&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;voterKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;makeVoterKey&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;// Register voter&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;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
            &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerVoter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;voterKey&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="c1"&gt;// Commit YES&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nullifierHash&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computeNullifierHash&lt;/span&gt;&lt;span class="p"&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;proposalId&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;voteCommitment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computeVoteCommitment&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="nx"&gt;blinder&lt;/span&gt;&lt;span class="p"&gt;);&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;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
            &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commitVote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nullifierHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;voteCommitment&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="c1"&gt;// Advance to reveal phase&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;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
            &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openRevealPhase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1101&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="c1"&gt;// Reveal YES&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;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
            &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revealVote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;n&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="nf"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentQueryContext&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="nf"&gt;expect&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;yesVotes&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalReveals&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&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;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rejects double-commit from same voter&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSimulator&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;voterKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;makeVoterKey&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="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;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
            &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerVoter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;voterKey&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;nullifierHash&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computeNullifierHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;makeSecret&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="nx"&gt;proposalId&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;voteCommitment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computeVoteCommitment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;makeBlinder&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="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;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
            &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commitVote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nullifierHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;voteCommitment&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commitVote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nullifierHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;voteCommitment&lt;/span&gt;&lt;span class="p"&gt;)&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Already committed&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;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rejects double-reveal&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSimulator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="c1"&gt;// ... register, commit, advance phase, first reveal succeeds&lt;/span&gt;
        &lt;span class="c1"&gt;// second reveal throws 'Vote already revealed'&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rejects reveal with wrong blinder&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSimulator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="c1"&gt;// ... commit with blinder A, attempt reveal with blinder B&lt;/span&gt;
        &lt;span class="c1"&gt;// throws 'Vote commitment mismatch'&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rejects commit from non-eligible voter&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSimulator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="c1"&gt;// Voter NOT in voterTree attempts to commit&lt;/span&gt;
        &lt;span class="c1"&gt;// throws 'Voter not in eligibility tree'&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tallies multiple voters correctly&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSimulator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="c1"&gt;// Register 3 voters, commit YES/YES/NO, advance, reveal all&lt;/span&gt;
        &lt;span class="c1"&gt;// expect yesVotes = 2n, noVotes = 1n&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;h2&gt;
  
  
  Example frontend
&lt;/h2&gt;

&lt;p&gt;The bounty requires an example frontend. Here's a minimal React component that walks a voter through all three phases:&lt;br&gt;
&lt;/p&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="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffect&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;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;const&lt;/span&gt; &lt;span class="nx"&gt;PHASES&lt;/span&gt; &lt;span class="o"&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;Commit&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;Reveal&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;Closed&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;VOTE_LABELS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Yes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Abstain&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&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;VotingPanel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VotingAPI&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setResults&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;phase&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="na"&gt;yes&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="na"&gt;no&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="na"&gt;abstain&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&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;refresh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setResults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getResults&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="nf"&gt;useEffect&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="nf"&gt;refresh&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleCommit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Generating ZK proof…&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;secret&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&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;Uint8Array&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blinder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&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;Uint8Array&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="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commitVote&lt;/span&gt;&lt;span class="p"&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;vote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;blinder&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Committed! Your &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;VOTE_LABELS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;vote&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt; vote is hidden until the reveal phase.`&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="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Error: &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;as&lt;/span&gt; &lt;span class="nb"&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="s2"&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;refresh&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleReveal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Generating reveal proof…&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="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revealVote&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Vote revealed and tallied!&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="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Error: &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;as&lt;/span&gt; &lt;span class="nb"&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="s2"&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;refresh&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;yes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;no&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;abstain&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;results&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="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&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="na"&gt;fontFamily&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;monospace&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Phase: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;PHASES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&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;phase&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Cast your vote. Your choice is hidden until the reveal phase.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onClick&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleCommit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
                            &lt;span class="na"&gt;style&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="na"&gt;marginRight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&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;VOTE_LABELS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
                        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;phase&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;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;The commit window has closed. Reveal your vote to have it counted.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleReveal&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Reveal My Vote&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Final Results&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Yes: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;yes&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;No: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;no&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Abstain: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;abstain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;style&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="na"&gt;marginTop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#666&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="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&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;The component checks phase on mount and after each action. In production you'd poll or subscribe to contract state updates. The ZK proof generation happens inside &lt;code&gt;api.commitVote()&lt;/code&gt; and &lt;code&gt;api.revealVote()&lt;/code&gt; — the UI just triggers the call and waits.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security model
&lt;/h2&gt;

&lt;p&gt;Before deploying anything, know what you're getting.&lt;/p&gt;

&lt;p&gt;Vote privacy during the commit phase holds because &lt;code&gt;persistentCommit&lt;/code&gt; with a random blinder is computationally hiding. Voter identity during the reveal phase holds because &lt;code&gt;persistentHash(voterSecret)&lt;/code&gt; doesn't identify anyone without the secret. Double-voting is prevented by nullifiers — one nullifier per voter, marked spent on first reveal. Tally integrity is enforced by ZK proof; the reveal circuit checks that the vote matches the stored commitment before incrementing anything.&lt;/p&gt;

&lt;p&gt;What this doesn't do: receipt-freeness. A voter can prove how they voted after the fact by sharing their &lt;code&gt;voterSecret&lt;/code&gt; and &lt;code&gt;blinder&lt;/code&gt;. That's inherent to commit/reveal — there's no way around it in this design. Participation is also observable: &lt;code&gt;totalCommits&lt;/code&gt; and &lt;code&gt;totalReveals&lt;/code&gt; are public counters, so anyone can see turnout even if they can't see individual choices. Without domain separation (see above), a voter using the same secret across multiple proposals has linkable nullifier hashes.&lt;/p&gt;

&lt;p&gt;None of these are bugs. They're trade-offs. Any scheme that needs to tally votes must eventually reveal them. The commit phase buys temporal privacy: nobody knows what anyone voted until the window opens. After that, votes are public by design.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;persistentHash&lt;/code&gt; for vote commitments — not hiding
&lt;/h3&gt;

&lt;p&gt;Three-option vote, three hashes. Any observer can crack a &lt;code&gt;persistentHash&lt;/code&gt; commitment before the reveal phase opens. Use &lt;code&gt;persistentCommit(vote, blinder)&lt;/code&gt; for anything that needs to stay hidden. Save &lt;code&gt;persistentHash&lt;/code&gt; for nullifiers, where binding is all that matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;checkRoot&lt;/code&gt; needs &lt;code&gt;disclose()&lt;/code&gt; on the computed digest
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;checkRoot&lt;/code&gt; is a ledger method. Any value derived from a witness that passes into a ledger method requires &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 plaintext"&gt;&lt;code&gt;const path     = getVoterPath();
const computed = merkleTreePathRoot&amp;lt;16, Bytes&amp;lt;32&amp;gt;&amp;gt;(path);

// Fails — computed is derived from the private witness path
voterTree.checkRoot(computed)

// Correct
voterTree.checkRoot(disclose(computed))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compiler error: &lt;code&gt;potential witness-value disclosure must be declared but is not — ledger operation might disclose a hash of the witness value&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This applies to both &lt;code&gt;MerkleTree&lt;/code&gt; and &lt;code&gt;HistoricMerkleTree&lt;/code&gt; — both use &lt;code&gt;checkRoot()&lt;/code&gt; in circuits. See also pitfall #9.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;code&gt;MerkleTree&lt;/code&gt; instead of &lt;code&gt;HistoricMerkleTree&lt;/code&gt; for voter registration
&lt;/h3&gt;

&lt;p&gt;Every new voter registration changes the root. Use a standard &lt;code&gt;MerkleTree&lt;/code&gt; and any voter who generated their proof before the latest registration now has an invalid proof. &lt;code&gt;HistoricMerkleTree&lt;/code&gt; validates against any past root, so concurrent registrations don't race with each other.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. &lt;code&gt;lookup()&lt;/code&gt; without a prior &lt;code&gt;member()&lt;/code&gt; check panics
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;commitments.lookup(key)&lt;/code&gt; panics at proof generation if the key is missing. Always check first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;assert(commitments.member(disclose(nullifierHash)), "No commitment found");
const stored = commitments.lookup(disclose(nullifierHash));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Missing &lt;code&gt;disclose()&lt;/code&gt; on exported parameters in ledger operations
&lt;/h3&gt;

&lt;p&gt;Exported circuit parameters — including publicly supplied values like &lt;code&gt;nullifierHash&lt;/code&gt; — need &lt;code&gt;disclose()&lt;/code&gt; before any ledger write or method call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Fails
commitments.insert(nullifierHash, voteCommitment);

// Correct
commitments.insert(disclose(nullifierHash), disclose(voteCommitment));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. &lt;code&gt;sealed export ledger&lt;/code&gt; is a parse error
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;sealed&lt;/code&gt; must come directly before &lt;code&gt;ledger&lt;/code&gt; — the &lt;code&gt;export&lt;/code&gt; modifier between them is invalid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sealed export ledger proposalId: Bytes&amp;lt;32&amp;gt;; // Parse error
sealed ledger proposalId: Bytes&amp;lt;32&amp;gt;;        // Correct
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  7. &lt;code&gt;pure&lt;/code&gt; circuits cannot access ledger fields
&lt;/h3&gt;

&lt;p&gt;Even a read-only ledger access makes a circuit impure in Compact. This is stricter than Solidity's &lt;code&gt;view&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;export pure circuit getPhase(): Uint&amp;lt;8&amp;gt; { return phase; } // Compile error
export circuit getPhase(): Uint&amp;lt;8&amp;gt; { return phase; }      // Correct
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  8. &lt;code&gt;Uint&amp;lt;64&amp;gt; + Uint&amp;lt;64&amp;gt;&lt;/code&gt; can't be assigned back to &lt;code&gt;Uint&amp;lt;64&amp;gt;&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Compact's integer types carry range information. Adding two &lt;code&gt;Uint&amp;lt;64&amp;gt;&lt;/code&gt; values produces a &lt;code&gt;Uint&amp;lt;0..2^65&amp;gt;&lt;/code&gt; — wider than &lt;code&gt;Uint&amp;lt;64&amp;gt;&lt;/code&gt; — and assigning that back to a &lt;code&gt;Uint&amp;lt;64&amp;gt;&lt;/code&gt; field fails at compile time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;expected right-hand side of = to have type Uint&amp;lt;64&amp;gt;
but received Uint&amp;lt;0..36893488147419103231&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do the deadline arithmetic off-chain in TypeScript and pass absolute block heights as constructor parameters. Don't compute &lt;code&gt;startBlock + duration&lt;/code&gt; inside the contract.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. &lt;code&gt;checkRootInHistory&lt;/code&gt; doesn't exist — use &lt;code&gt;checkRoot&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;It seems like it should exist. It doesn't. Both &lt;code&gt;MerkleTree&lt;/code&gt; and &lt;code&gt;HistoricMerkleTree&lt;/code&gt; use &lt;code&gt;checkRoot()&lt;/code&gt; in Compact circuits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;operation checkRootInHistory undefined for ledger field type HistoricMerkleTree&amp;lt;16, Bytes&amp;lt;32&amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;checkRoot(disclose(computed))&lt;/code&gt; for both tree types. The distinction between the two is in the TypeScript client API — &lt;code&gt;HistoricMerkleTree&lt;/code&gt; exposes past roots so off-chain proof generation can pick one that's still valid.&lt;/p&gt;

&lt;h3&gt;
  
  
  10. Branching on exported parameters without &lt;code&gt;disclose()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Compact treats exported circuit parameters as potentially private until explicitly disclosed. If you branch on a parameter and execute different ledger operations per branch, the compiler flags it: which branch ran reveals the parameter's value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Fails — branching on `vote` discloses which increment runs
if (vote == 0) { noVotes.increment(1); }
else if (vote == 1) { yesVotes.increment(1); }
else { abstainVotes.increment(1); }
&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: voting.compact line 158 char 16:
  potential witness-value disclosure must be declared but is not:
    the value of parameter vote of exported circuit revealVote
    performing this ledger operation might disclose the boolean value
    of the result of a comparison involving the witness value
    via: the comparison at line 157, the conditional branch at line 157
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is &lt;code&gt;disclose()&lt;/code&gt; on the parameter inside the condition, explicitly acknowledging that the branch is intentional:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Correct — vote is intentionally public at reveal time
if (disclose(vote) == 0) { noVotes.increment(1); }
else if (disclose(vote) == 1) { yesVotes.increment(1); }
else { abstainVotes.increment(1); }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This applies to &lt;code&gt;assert&lt;/code&gt; comparisons too: &lt;code&gt;assert(disclose(vote) &amp;lt; 3, ...)&lt;/code&gt;. The rule is consistent — any expression involving an exported parameter that drives different execution paths requires &lt;code&gt;disclose()&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  11. Not separating nullifiers across proposals
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;persistentHash(voterSecret)&lt;/code&gt; is deterministic across every contract that uses it. Same voter, same secret, same nullifier hash in every proposal — and an observer can link them all. Mix the proposal ID into the nullifier derivation off-chain using the pattern described in the domain separation section above.&lt;/p&gt;




&lt;h2&gt;
  
  
  Compile and test
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;compact compile voting.compact managed/voting
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; vitest @midnight-ntwrk/compact-runtime
npx vitest run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full contract is in the companion repository. A GitHub Actions workflow compiles it on every push and uploads the artifacts — the run linked below shows a clean compile with all 11 pitfalls already fixed:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/IamHarrie-Labs/compact-voting-guide/actions/runs/25685662818" rel="noopener noreferrer"&gt;https://github.com/IamHarrie-Labs/compact-voting-guide/actions/runs/25685662818&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.midnight.network/develop/reference/compact/lang-ref" rel="noopener noreferrer"&gt;Compact Language Reference&lt;/a&gt; — full ledger type spec, circuit constraints&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.midnight.network/compact/standard-library/exports" rel="noopener noreferrer"&gt;Standard Library Exports&lt;/a&gt; — &lt;code&gt;persistentHash&lt;/code&gt;, &lt;code&gt;persistentCommit&lt;/code&gt;, &lt;code&gt;MerkleTreePath&lt;/code&gt;, &lt;code&gt;HistoricMerkleTree&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.midnight.network/tutorials/bboard/smart-contract" rel="noopener noreferrer"&gt;Bulletin Board Tutorial&lt;/a&gt; — canonical reference for witness patterns&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.midnight.network/relnotes/compact" rel="noopener noreferrer"&gt;Compact Release Notes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;All Compact examples in this article were compiled and verified against the latest Compact compiler via GitHub Actions. Source and CI run: &lt;a href="https://github.com/IamHarrie-Labs/compact-voting-guide/actions/runs/25685662818" rel="noopener noreferrer"&gt;IamHarrie-Labs/compact-voting-guide&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>midnightfordevs</category>
      <category>blockchain</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Working with Maps and Merkle Trees in Compact</title>
      <dc:creator>Harrie</dc:creator>
      <pubDate>Mon, 11 May 2026 08:49:11 +0000</pubDate>
      <link>https://dev.to/iamharrie/working-with-maps-and-merkle-trees-in-compact-343k</link>
      <guid>https://dev.to/iamharrie/working-with-maps-and-merkle-trees-in-compact-343k</guid>
      <description>&lt;p&gt;Compact gives you two ways to store collections on-chain: &lt;code&gt;Map&lt;/code&gt; and &lt;code&gt;MerkleTree&lt;/code&gt;. They look superficially similar (both key-based, both ledger types) but solve completely different problems. Picking the wrong one will cost you.&lt;/p&gt;

&lt;p&gt;This tutorial walks through both from scratch. You'll build a Map-based registry, a Merkle-tree allowlist with depth-20 path verification, and a hybrid contract that combines both. All contracts are verified against Compact compiler v0.31.0.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; Midnight toolchain installed, basic familiarity with Compact circuits and ledger declarations.&lt;/p&gt;




&lt;h2&gt;
  
  
  The core trade-off
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;Map&amp;lt;K, V&amp;gt;&lt;/code&gt; stores key-value pairs directly on-chain. Every entry is readable by anyone — it's public state. You get O(1) lookups, full CRUD operations, and simple iteration in TypeScript. The trade-off: the entire dataset is visible.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;MerkleTree&amp;lt;n, T&amp;gt;&lt;/code&gt; stores only its root hash on-chain. Individual entries aren't visible. Membership is proven through a cryptographic path: a witness that shows "this leaf exists in a tree with this root" without revealing which leaf or the rest of the tree. The trade-off: it's append-only, deletion is complicated, and path lookups are O(n) without index tracking.&lt;/p&gt;

&lt;p&gt;Short version: use &lt;code&gt;Map&lt;/code&gt; when you need mutable, readable data. Use &lt;code&gt;MerkleTree&lt;/code&gt; when you need to prove membership without revealing the full set.&lt;/p&gt;




&lt;h2&gt;
  
  
  Maps in Compact
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Declaration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma language_version &amp;gt;= 0.20;
import CompactStandardLibrary;

export ledger registry: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Bytes&amp;lt;32&amp;gt;&amp;gt;;
export ledger balances: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Uint&amp;lt;128&amp;gt;&amp;gt;;
export ledger approvals: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Boolean&amp;gt;&amp;gt;;  // nested
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Maps are ledger-state types. The key and value can be any Compact type, or another ledger-state type (which allows nesting). The only restriction: &lt;code&gt;Opaque&amp;lt;"string"&amp;gt;&lt;/code&gt; and &lt;code&gt;Opaque&amp;lt;"Uint8Array"&amp;gt;&lt;/code&gt; can't be keys.&lt;/p&gt;

&lt;h3&gt;
  
  
  The full Map API
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Check existence
registry.member(key)                    // Boolean

// Read
registry.lookup(key)                    // returns V (fails if missing — guard with member() first)

// Write
registry.insert(disclose(key), disclose(value))   // add or overwrite
registry.remove(disclose(key))                    // delete

// Size
registry.size()                         // Uint&amp;lt;64&amp;gt;
registry.isEmpty()                      // Boolean

// Default insertion (for nested maps)
registry.insertDefault(disclose(key))   // inserts default&amp;lt;V&amp;gt; at key
registry.resetToDefault()               // clear all entries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why &lt;code&gt;disclose()&lt;/code&gt; on write operations
&lt;/h3&gt;

&lt;p&gt;Any circuit parameter going into a ledger write must be wrapped in &lt;code&gt;disclose()&lt;/code&gt;. This applies to both keys and values. Without it, the compiler rejects the code with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Exception: potential witness-value disclosure must be declared but is not
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule covers all exported circuit parameters, not just witness data. The compiler can't statically prove where a value came from, so it treats all exported inputs as potentially private and requires explicit disclosure before they touch public ledger state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Fails
registry.insert(key, value);

// Correct
registry.insert(disclose(key), disclose(value));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Sealed maps
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;sealed&lt;/code&gt; modifier locks a ledger field after the constructor runs. Nothing can change it afterward. Not through any circuit, not through any future transaction. Use it for configuration that should be set once at deployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sealed ledger adminKey: Bytes&amp;lt;32&amp;gt;;
export ledger config: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Bytes&amp;lt;32&amp;gt;&amp;gt;;

constructor(admin: Bytes&amp;lt;32&amp;gt;) {
    adminKey = disclose(admin);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any attempt to write to &lt;code&gt;adminKey&lt;/code&gt; outside the constructor is a compile error. This is a stronger guarantee than access control logic: the constraint is in the language itself, not in the contract logic.&lt;/p&gt;

&lt;p&gt;One syntax note: &lt;code&gt;sealed&lt;/code&gt; takes the &lt;code&gt;ledger&lt;/code&gt; keyword directly — &lt;code&gt;sealed ledger name: Type&lt;/code&gt;. Adding &lt;code&gt;export&lt;/code&gt; between them (&lt;code&gt;sealed export ledger&lt;/code&gt;) is a parse error. Sealed fields are part of the ledger state and readable on-chain regardless of whether they carry the &lt;code&gt;export&lt;/code&gt; modifier.&lt;/p&gt;




&lt;h2&gt;
  
  
  Merkle trees in Compact
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Declaration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export ledger allowlist: MerkleTree&amp;lt;20, Bytes&amp;lt;32&amp;gt;&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first parameter is the tree depth. Depth 20 supports up to 2^20 = 1,048,576 leaves. The compiler enforces depth between 2 and 32 inclusive. The second parameter is the leaf type: any Compact type except &lt;code&gt;Opaque&amp;lt;"string"&amp;gt;&lt;/code&gt; or &lt;code&gt;Opaque&amp;lt;"Uint8Array"&amp;gt;&lt;/code&gt; (they can't be hashed by the standard library).&lt;/p&gt;

&lt;h3&gt;
  
  
  Inserting leaves
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Insert a leaf (appends at the next free slot)
allowlist.insert(disclose(leaf));

// Insert a pre-hashed value (skips leaf hashing)
allowlist.insertHash(disclose(hash));

// Insert at a specific index (useful when you track indices off-chain)
allowlist.insertIndex(disclose(leaf), disclose(index));

// Insert default value at index (logical "deletion" workaround)
allowlist.insertIndexDefault(disclose(index));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MerkleTrees are append-only. There's no &lt;code&gt;.remove()&lt;/code&gt;. The pattern for "deleting" a leaf is inserting a sentinel default value at that index. This changes the root and invalidates any outstanding proofs for that slot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Path verification
&lt;/h3&gt;

&lt;p&gt;The proof model: the tree root lives on-chain. The membership path (leaf + sibling hashes + directions) stays off-chain, provided by a witness. The circuit reconstructs the root from the path and checks it against the on-chain root.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;witness findLeaf(leaf: Bytes&amp;lt;32&amp;gt;): MerkleTreePath&amp;lt;20, Bytes&amp;lt;32&amp;gt;&amp;gt;;

export circuit verify(leaf: Bytes&amp;lt;32&amp;gt;): [] {
    const path = findLeaf(leaf);
    const computed = merkleTreePathRoot&amp;lt;20, Bytes&amp;lt;32&amp;gt;&amp;gt;(path);
    assert(
        allowlist.checkRoot(disclose(computed)),
        "Leaf is not in the allowlist"
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;checkRoot(MerkleTreeDigest)&lt;/code&gt; is a method on the &lt;code&gt;MerkleTree&lt;/code&gt; ledger type. It returns &lt;code&gt;true&lt;/code&gt; if the given digest matches the tree's current root. This is cleaner than storing and comparing the root manually.&lt;/p&gt;

&lt;h3&gt;
  
  
  HistoricMerkleTree for concurrent inserts
&lt;/h3&gt;

&lt;p&gt;Standard &lt;code&gt;MerkleTree&lt;/code&gt; proofs validate against the current root. In a busy contract where multiple users are inserting simultaneously, a proof generated before an insert will fail after it. The root changed. This bites almost everyone who first deploys a real allowlist under concurrent load.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;HistoricMerkleTree&amp;lt;n, T&amp;gt;&lt;/code&gt; keeps a history of past roots. A witness can prove membership against any past root, not just the latest one. This makes it resilient to concurrent insertions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export ledger members: HistoricMerkleTree&amp;lt;20, Bytes&amp;lt;32&amp;gt;&amp;gt;;

witness getMemberPath(): MerkleTreePath&amp;lt;20, Bytes&amp;lt;32&amp;gt;&amp;gt;;

export circuit addMember(leaf: Bytes&amp;lt;32&amp;gt;): [] {
    members.insert(disclose(leaf));
}

export circuit proveHistoricMembership(): [] {
    const path = getMemberPath();
    const computed = merkleTreePathRoot&amp;lt;20, Bytes&amp;lt;32&amp;gt;&amp;gt;(path);
    assert(
        members.checkRootInHistory(disclose(computed)),
        "Leaf is not a historic member"
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;checkRootInHistory(digest)&lt;/code&gt; returns &lt;code&gt;true&lt;/code&gt; if the digest matches any root the tree has ever had. Use &lt;code&gt;HistoricMerkleTree&lt;/code&gt; for allowlists that see frequent insertions, or any pattern where proof generation and verification don't happen atomically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Example 1: Map-based registry
&lt;/h2&gt;

&lt;p&gt;A contract where addresses can register key-value entries, update them, and remove them.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;registry.compact&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma language_version &amp;gt;= 0.20;
import CompactStandardLibrary;

export ledger registry: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Bytes&amp;lt;32&amp;gt;&amp;gt;;
export ledger entryCount: Counter;

export circuit register(key: Bytes&amp;lt;32&amp;gt;, value: Bytes&amp;lt;32&amp;gt;): [] {
    assert(!registry.member(disclose(key)), "Key already registered");
    registry.insert(disclose(key), disclose(value));
    entryCount.increment(1);
}

export circuit update(key: Bytes&amp;lt;32&amp;gt;, newValue: Bytes&amp;lt;32&amp;gt;): [] {
    assert(registry.member(disclose(key)), "Key not registered");
    registry.insert(disclose(key), disclose(newValue));
}

export circuit deregister(key: Bytes&amp;lt;32&amp;gt;): [] {
    assert(registry.member(disclose(key)), "Key not registered");
    registry.remove(disclose(key));
}

export circuit isRegistered(key: Bytes&amp;lt;32&amp;gt;): Boolean {
    return registry.member(disclose(key));
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;isRegistered&lt;/code&gt; only reads ledger state and calls no witnesses, but it still accesses the &lt;code&gt;registry&lt;/code&gt; ledger field — which means it cannot be marked &lt;code&gt;pure&lt;/code&gt;. In Compact, &lt;code&gt;pure&lt;/code&gt; circuits have strict semantics: zero ledger access, zero witness calls, computation on input parameters only. This is stricter than Solidity's &lt;code&gt;view&lt;/code&gt; functions. &lt;code&gt;isRegistered&lt;/code&gt; is a regular exported circuit that reads without writing.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;registry.test.ts&lt;/code&gt;
&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;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&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;vitest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;createConstructorContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createCircuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;sampleContractAddress&lt;/span&gt;&lt;span class="p"&gt;,&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-ntwrk/compact-runtime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ledger&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;../managed/registry/contract/index.js&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;coinPublicKey&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;Uint8Array&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contractAddress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sampleContractAddress&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;createSimulator&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;contract&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;Contract&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;constructorCtx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createConstructorContext&lt;/span&gt;&lt;span class="p"&gt;({},&lt;/span&gt; &lt;span class="nx"&gt;coinPublicKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;currentPrivateState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentContractState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentZswapLocalState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;constructorCtx&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;circuitContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createCircuitContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;contractAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;currentZswapLocalState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;currentContractState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;currentPrivateState&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="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;circuitContext&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;keyA&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;Uint8Array&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="nf"&gt;fill&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;valueA&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;Uint8Array&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="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;valueB&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;Uint8Array&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="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keyB&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;Uint8Array&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="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;registry&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;registers a new entry and increments the counter&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSimulator&lt;/span&gt;&lt;span class="p"&gt;();&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;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&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;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keyA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;valueA&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="nf"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentQueryContext&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="nf"&gt;expect&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;entryCount&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;member&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyA&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&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="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rejects duplicate registration&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSimulator&lt;/span&gt;&lt;span class="p"&gt;();&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;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&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;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keyA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;valueA&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&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;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keyA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;valueA&lt;/span&gt;&lt;span class="p"&gt;)&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="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;updates an existing entry&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSimulator&lt;/span&gt;&lt;span class="p"&gt;();&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;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&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;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keyA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;valueA&lt;/span&gt;&lt;span class="p"&gt;));&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;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keyA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;valueB&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="nf"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentQueryContext&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="nf"&gt;expect&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;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyA&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;valueB&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rejects update for unregistered key&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSimulator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keyA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;valueB&lt;/span&gt;&lt;span class="p"&gt;)&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="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deregisters an entry&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSimulator&lt;/span&gt;&lt;span class="p"&gt;();&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;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&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;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keyA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;valueA&lt;/span&gt;&lt;span class="p"&gt;));&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;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deregister&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keyA&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="nf"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentQueryContext&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="nf"&gt;expect&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;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;member&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyA&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&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="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reports membership correctly&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSimulator&lt;/span&gt;&lt;span class="p"&gt;();&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;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&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;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keyA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;valueA&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isA&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isRegistered&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keyA&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isB&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isRegistered&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keyB&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isA&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isB&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&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="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Example 2: Merkle-tree allowlist (depth 20)
&lt;/h2&gt;

&lt;p&gt;A contract that manages a membership allowlist. Members can be added. Anyone can generate a ZK proof of membership without revealing which slot they occupy or who else is in the list.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;allowlist.compact&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma language_version &amp;gt;= 0.20;
import CompactStandardLibrary;

export ledger allowlist: MerkleTree&amp;lt;20, Bytes&amp;lt;32&amp;gt;&amp;gt;;
export ledger leafCount: Counter;

// Off-chain witness: given a leaf, return its Merkle path
witness findLeaf(leaf: Bytes&amp;lt;32&amp;gt;): MerkleTreePath&amp;lt;20, Bytes&amp;lt;32&amp;gt;&amp;gt;;

export circuit addToAllowlist(leaf: Bytes&amp;lt;32&amp;gt;): [] {
    allowlist.insert(disclose(leaf));
    leafCount.increment(1);
}

// Prove membership without revealing which leaf or its position
export circuit verifyMembership(leaf: Bytes&amp;lt;32&amp;gt;): [] {
    const path = findLeaf(leaf);
    const computed = merkleTreePathRoot&amp;lt;20, Bytes&amp;lt;32&amp;gt;&amp;gt;(path);
    assert(
        allowlist.checkRoot(disclose(computed)),
        "Leaf is not in the allowlist"
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;findLeaf&lt;/code&gt; witness runs entirely off-chain in TypeScript. It has access to the full tree state (through &lt;code&gt;context.ledger.allowlist&lt;/code&gt;) and returns the path for the requested leaf. No path data touches the on-chain state: only the root comparison happens in the circuit.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;allowlist.test.ts&lt;/code&gt;
&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;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&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;vitest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;createConstructorContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createCircuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;sampleContractAddress&lt;/span&gt;&lt;span class="p"&gt;,&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-ntwrk/compact-runtime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ledger&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;../managed/allowlist/contract/index.js&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;coinPublicKey&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;Uint8Array&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contractAddress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sampleContractAddress&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;createSimulator&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;contract&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;Contract&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;findLeaf&lt;/span&gt;&lt;span class="p"&gt;:&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="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;leaf&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="o"&gt;=&amp;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;path&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;ledger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowlist&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findPathForLeaf&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="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;path&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="s1"&gt;Leaf not found in allowlist&lt;/span&gt;&lt;span class="dl"&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="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="nx"&gt;path&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;constructorCtx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createConstructorContext&lt;/span&gt;&lt;span class="p"&gt;({},&lt;/span&gt; &lt;span class="nx"&gt;coinPublicKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;currentPrivateState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentContractState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentZswapLocalState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;constructorCtx&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;circuitContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createCircuitContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;contractAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;currentZswapLocalState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;currentContractState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;currentPrivateState&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="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;circuitContext&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;leafA&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;Uint8Array&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="nf"&gt;fill&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;leafB&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;Uint8Array&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="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;leafC&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;Uint8Array&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="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;allowlist&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;adds a leaf and increments the counter&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSimulator&lt;/span&gt;&lt;span class="p"&gt;();&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;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addToAllowlist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;leafA&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="nf"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentQueryContext&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="nf"&gt;expect&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;leafCount&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&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;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;verifies a leaf that is in the allowlist&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSimulator&lt;/span&gt;&lt;span class="p"&gt;();&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;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addToAllowlist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;leafA&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verifyMembership&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;leafA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;not&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="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rejects a leaf not in the allowlist&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSimulator&lt;/span&gt;&lt;span class="p"&gt;();&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;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addToAllowlist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;leafA&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verifyMembership&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;leafB&lt;/span&gt;&lt;span class="p"&gt;)&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="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;verifies multiple members independently&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSimulator&lt;/span&gt;&lt;span class="p"&gt;();&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;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addToAllowlist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;leafA&lt;/span&gt;&lt;span class="p"&gt;));&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;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addToAllowlist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;leafB&lt;/span&gt;&lt;span class="p"&gt;));&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;circuitContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addToAllowlist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;leafC&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verifyMembership&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;leafA&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;not&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verifyMembership&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;leafB&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;not&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;impureCircuits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verifyMembership&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuitContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;leafC&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;not&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="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Off-chain witness implementation
&lt;/h3&gt;

&lt;p&gt;When you deploy this contract, you provide the &lt;code&gt;findLeaf&lt;/code&gt; implementation in TypeScript. The compact runtime calls it during proof generation:&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="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;WitnessContext&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-ntwrk/compact-runtime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PrivateState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;never&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;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="na"&gt;findLeaf&lt;/span&gt;&lt;span class="p"&gt;:&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;PrivateState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;leaf&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="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="nx"&gt;MerkleTreePath&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="c1"&gt;// findPathForLeaf does O(n) scan — use pathForLeaf(index, leaf) if you track indices&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&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;ledger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowlist&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findPathForLeaf&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="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;path&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="s1"&gt;Leaf not found&lt;/span&gt;&lt;span class="dl"&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="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="nx"&gt;path&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;If your allowlist is large and you need O(1) path lookups, track leaf indices in your application state and use &lt;code&gt;context.ledger.allowlist.pathForLeaf(index, leaf)&lt;/code&gt; instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hybrid pattern: Map + MerkleTree together
&lt;/h2&gt;

&lt;p&gt;Neither structure handles every use case on its own. A common pattern in production contracts is to use a &lt;code&gt;Map&lt;/code&gt; for O(1) reads and mutations, while maintaining a &lt;code&gt;MerkleTree&lt;/code&gt; for ZK membership proofs. This gives you fast on-chain lookups and privacy-preserving membership proofs from the same dataset.&lt;/p&gt;

&lt;p&gt;Here's an identity registry that stores profile data in a Map (readable, mutable) and provides ZK membership proofs via a MerkleTree (private, unlinkable):&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;identity-registry.compact&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma language_version &amp;gt;= 0.20;
import CompactStandardLibrary;

// Map: full profile data — readable by anyone on-chain
export ledger profiles: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Bytes&amp;lt;32&amp;gt;&amp;gt;;

// MerkleTree: membership set — prove you're registered without revealing your key
export ledger memberTree: MerkleTree&amp;lt;20, Bytes&amp;lt;32&amp;gt;&amp;gt;;
export ledger memberCount: Counter;

// Sealed admin key — set at deployment, immutable afterward
sealed ledger adminKey: Bytes&amp;lt;32&amp;gt;;

witness getMemberPath(identity: Bytes&amp;lt;32&amp;gt;): MerkleTreePath&amp;lt;20, Bytes&amp;lt;32&amp;gt;&amp;gt;;

constructor(admin: Bytes&amp;lt;32&amp;gt;) {
    adminKey = disclose(admin);
}

// Register: add profile data to Map AND add identity hash to MerkleTree
export circuit registerIdentity(identity: Bytes&amp;lt;32&amp;gt;, profileData: Bytes&amp;lt;32&amp;gt;): [] {
    assert(!profiles.member(disclose(identity)), "Identity already registered");
    profiles.insert(disclose(identity), disclose(profileData));
    memberTree.insert(disclose(identity));
    memberCount.increment(1);
}

// Read profile — public, anyone can query (not pure: reads ledger field)
export circuit getProfile(identity: Bytes&amp;lt;32&amp;gt;): Bytes&amp;lt;32&amp;gt; {
    assert(profiles.member(disclose(identity)), "Identity not found");
    return profiles.lookup(disclose(identity));
}

// Update profile data — only the identity holder can update (they know their key)
export circuit updateProfile(identity: Bytes&amp;lt;32&amp;gt;, newData: Bytes&amp;lt;32&amp;gt;): [] {
    assert(profiles.member(disclose(identity)), "Identity not found");
    profiles.insert(disclose(identity), disclose(newData));
}

// Prove membership without revealing which identity or correlating to profile
export circuit proveMembership(identity: Bytes&amp;lt;32&amp;gt;): [] {
    const path = getMemberPath(identity);
    const computed = merkleTreePathRoot&amp;lt;20, Bytes&amp;lt;32&amp;gt;&amp;gt;(path);
    assert(
        memberTree.checkRoot(disclose(computed)),
        "Identity is not a registered member"
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The separation matters here. An on-chain observer can read any profile from the &lt;code&gt;Map&lt;/code&gt;, but they can't link a membership proof to a specific profile entry. The &lt;code&gt;MerkleTree&lt;/code&gt; proof reveals nothing about which identity was used. Two views of the same dataset, each useful in different contexts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Choosing the right structure
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Store user balances, settings, or metadata&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Map&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;O(1) lookups by key&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Map&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full CRUD (create, read, update, delete)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Map&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Iterate all entries in TypeScript&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Map&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prove membership without revealing identity&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MerkleTree&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large sets where full on-chain storage is expensive&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MerkleTree&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Allowlists, credential sets, nullifier sets&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MerkleTree&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resilience to concurrent insertions&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HistoricMerkleTree&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Both readable data AND ZK membership proofs&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Map&lt;/code&gt; + &lt;code&gt;MerkleTree&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Immutable configuration set at deployment&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sealed ledger&lt;/code&gt; field&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The most common mistake is using &lt;code&gt;MerkleTree&lt;/code&gt; for data you actually need to read. It only stores the root, so you can't retrieve individual entries from the chain. The tree data lives off-chain with the contract operator.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;lookup()&lt;/code&gt; on a missing key panics
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;lookup(key)&lt;/code&gt; doesn't return a &lt;code&gt;Maybe&lt;/code&gt; — it panics at proof generation if the key doesn't exist. Always guard with &lt;code&gt;member()&lt;/code&gt; first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Unsafe
const val = registry.lookup(disclose(key));

// Safe
assert(registry.member(disclose(key)), "Key does not exist");
const val = registry.lookup(disclose(key));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Forgetting &lt;code&gt;disclose()&lt;/code&gt; on writes
&lt;/h3&gt;

&lt;p&gt;Every write to a &lt;code&gt;Map&lt;/code&gt; or &lt;code&gt;MerkleTree&lt;/code&gt; from an exported circuit requires &lt;code&gt;disclose()&lt;/code&gt; on the keys and values. The compiler error is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;potential witness-value disclosure must be declared but is not
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This applies even to plain circuit parameters, not just witness data. The fix: wrap any value at the point it hits the ledger.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;code&gt;Opaque&lt;/code&gt; types can't be Map keys or MerkleTree leaves
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Opaque&amp;lt;"string"&amp;gt;&lt;/code&gt; and &lt;code&gt;Opaque&amp;lt;"Uint8Array"&amp;gt;&lt;/code&gt; are not hashable by the standard library. If your data is opaque, hash it first with &lt;code&gt;persistentHash&lt;/code&gt; and use the resulting &lt;code&gt;Bytes&amp;lt;32&amp;gt;&lt;/code&gt; as the key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const keyHash = persistentHash&amp;lt;Opaque&amp;lt;"string"&amp;gt;&amp;gt;(rawKey);
registry.insert(disclose(keyHash), disclose(value));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. MerkleTree deletion doesn't work the way you expect
&lt;/h3&gt;

&lt;p&gt;There's no &lt;code&gt;.remove()&lt;/code&gt; on &lt;code&gt;MerkleTree&lt;/code&gt;. The standard workaround is inserting a sentinel default value at the target index, which invalidates any existing proof for that slot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// "Delete" leaf at known index by overwriting with default
allowlist.insertIndexDefault(disclose(index));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Track leaf indices off-chain if you need to delete. If you need true deletion semantics, consider using a &lt;code&gt;Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Boolean&amp;gt;&lt;/code&gt; with a tombstone pattern instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Marking a ledger-reading circuit as &lt;code&gt;pure&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;pure&lt;/code&gt; circuits in Compact can't access ledger fields at all — not even for reads. The compiler catches it immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;circuit isRegistered is marked pure but is actually impure because it accesses ledger field registry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is stricter than Solidity's &lt;code&gt;view&lt;/code&gt; functions. If your circuit reads from a &lt;code&gt;Map&lt;/code&gt; or &lt;code&gt;MerkleTree&lt;/code&gt; ledger field, it must be a regular &lt;code&gt;export circuit&lt;/code&gt;. Reserve &lt;code&gt;pure&lt;/code&gt; for circuits that only do computation on their input parameters with no external state.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. &lt;code&gt;checkRoot()&lt;/code&gt; requires &lt;code&gt;disclose()&lt;/code&gt; on the computed digest
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;checkRoot&lt;/code&gt; and &lt;code&gt;checkRootInHistory&lt;/code&gt; are ledger operations. The compiler requires &lt;code&gt;disclose()&lt;/code&gt; on any witness-derived value passed into a ledger call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;potential witness-value disclosure must be declared but is not:
  ledger operation might disclose a hash of the witness value
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Fails
assert(allowlist.checkRoot(computed), "...");

// Correct
assert(allowlist.checkRoot(disclose(computed)), "...");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks counterintuitive — you're not "publishing" the computed root, you're just telling the compiler: "I know this value derived from a witness is passing into a ledger operation, and that's intentional." The actual membership proof remains private.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. &lt;code&gt;sealed export ledger&lt;/code&gt; is a parse error
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;sealed&lt;/code&gt; modifier must be followed directly by &lt;code&gt;ledger&lt;/code&gt;. No &lt;code&gt;export&lt;/code&gt; between them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Parse error
sealed export ledger adminKey: Bytes&amp;lt;32&amp;gt;;

// Correct
sealed ledger adminKey: Bytes&amp;lt;32&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sealed fields are still part of on-chain state and readable regardless of whether they carry the &lt;code&gt;export&lt;/code&gt; modifier.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Using &lt;code&gt;MerkleTree&lt;/code&gt; for data you need to read back
&lt;/h3&gt;

&lt;p&gt;The on-chain &lt;code&gt;MerkleTree&lt;/code&gt; ledger type stores only the root. You cannot read individual leaves from the chain. If you need to enumerate members or look up entries, use &lt;code&gt;Map&lt;/code&gt;. If you need both, combine them as shown in the hybrid example.&lt;/p&gt;




&lt;h2&gt;
  
  
  Compile and test
&lt;/h2&gt;

&lt;p&gt;Create a project directory with the three contracts above. Compile each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;compact compile registry.compact managed/registry
compact compile allowlist.compact managed/allowlist
compact compile identity-registry.compact managed/identity-registry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install test dependencies and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; vitest @midnight-ntwrk/compact-runtime
npx vitest run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three contracts in this article were compiled and verified against &lt;strong&gt;Compact compiler v0.31.0&lt;/strong&gt; via GitHub Actions. You can inspect the passing workflow run and download the compiled artifacts at: &lt;a href="https://github.com/IamHarrie-Labs/compact-maps-merkle-guide" rel="noopener noreferrer"&gt;https://github.com/IamHarrie-Labs/compact-maps-merkle-guide&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.midnight.network/develop/reference/compact/lang-ref" rel="noopener noreferrer"&gt;Compact Language Reference&lt;/a&gt; — Map, MerkleTree, sealed modifier full spec&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.midnight.network/compact/standard-library/exports" rel="noopener noreferrer"&gt;Standard Library Exports&lt;/a&gt; — &lt;code&gt;merkleTreePathRoot&lt;/code&gt;, &lt;code&gt;MerkleTreePath&lt;/code&gt;, &lt;code&gt;MerkleTreeDigest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.midnight.network/tutorials/bboard/smart-contract" rel="noopener noreferrer"&gt;Bulletin Board Tutorial&lt;/a&gt; — canonical reference for witness patterns&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.midnight.network/relnotes/compact" rel="noopener noreferrer"&gt;Compact Release Notes&lt;/a&gt; — compiler changelog&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;All Compact examples in this article were compiled and verified against Compact compiler v0.31.0. Source: &lt;a href="https://github.com/IamHarrie-Labs/compact-maps-merkle-guide" rel="noopener noreferrer"&gt;IamHarrie-Labs/compact-maps-merkle-guide&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>midnightfordevs</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Thinking in Compact: A guide for Circom developers</title>
      <dc:creator>Harrie</dc:creator>
      <pubDate>Mon, 04 May 2026 21:06:14 +0000</pubDate>
      <link>https://dev.to/iamharrie/thinking-in-compact-a-guide-for-circom-developers-3pf9</link>
      <guid>https://dev.to/iamharrie/thinking-in-compact-a-guide-for-circom-developers-3pf9</guid>
      <description>&lt;p&gt;If you've built ZK circuits with Circom before, you already have a head start with Midnight's Compact language. The core instinct is the same: express computation in a way a prover can prove and a verifier can verify, without leaking private data. What changes is the &lt;em&gt;shape&lt;/em&gt; of that instinct — Compact is a full contract language, not a circuit DSL, and that difference runs deeper than syntax.&lt;/p&gt;

&lt;p&gt;This isn't a beginner's intro to zero-knowledge proofs. It's a translation guide for someone who already thinks in signals and templates and wants to understand what those map to when writing Compact contracts on Midnight. The areas that trip people up most: concept mapping, the ledger (nothing like it exists in Circom), and a handful of pitfalls that catch almost everyone on the transition.&lt;/p&gt;




&lt;h2&gt;
  
  
  Different problem, similar intuition
&lt;/h2&gt;

&lt;p&gt;Circom does one thing: describe a constraint system. You define signals, wire them together, and the Circom compiler turns your templates into an R1CS (Rank-1 Constraint System) that snarkjs or Groth16 uses to produce and verify proofs. Everything else — state management, contract logic, user interaction — lives in Solidity or some wrapper layer you build separately.&lt;/p&gt;

&lt;p&gt;Compact is a full contract language. It's built for Midnight, a blockchain with privacy at the protocol level. A Compact contract contains your ZK circuit logic, your on-chain state machine, and the interface your DApp calls into. No Solidity contract needed on top.&lt;/p&gt;

&lt;p&gt;The underlying proof system is also different. Circom compiles to R1CS. Compact compiles to ZKIR, Midnight's own intermediate representation. You can't port a Circom circuit by copy-pasting — the constraint models operate at different abstractions. But your &lt;em&gt;thinking&lt;/em&gt; transfers well. The same habits that made you effective in Circom apply in Compact.&lt;/p&gt;




&lt;h2&gt;
  
  
  Your Circom vocabulary, translated
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Circom concept&lt;/th&gt;
&lt;th&gt;Compact equivalent&lt;/th&gt;
&lt;th&gt;Key difference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;signal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Typed variable&lt;/td&gt;
&lt;td&gt;Rich type system: &lt;code&gt;Field&lt;/code&gt;, &lt;code&gt;Uint&amp;lt;n&amp;gt;&lt;/code&gt;, &lt;code&gt;Bytes&amp;lt;n&amp;gt;&lt;/code&gt;, &lt;code&gt;Vector&amp;lt;n, T&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;template&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;circuit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Can be &lt;code&gt;pure&lt;/code&gt; (stateless) or impure (reads/writes ledger)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;component&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Circuit call + witness&lt;/td&gt;
&lt;td&gt;Sub-circuits called directly; private data comes from witnesses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;===&lt;/code&gt; R1CS constraint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;assert(cond, msg)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same constraint logic, more readable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;==&lt;/code&gt; signal assignment&lt;/td&gt;
&lt;td&gt;Variable assignment + &lt;code&gt;disclose()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Privacy is explicit — you declare when private data goes public&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;component main&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;export circuit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Exported circuits are the contract's public entry points&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&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%2Fe8yso80k7ju5wzewlx5k.png" alt=" " width="800" height="450"&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Signals → typed variables
&lt;/h3&gt;

&lt;p&gt;In Circom, a signal is always a field element — full stop. If you want a boolean, you write constraints enforcing it's 0 or 1. Range checks require bit decomposition. You do that work manually.&lt;/p&gt;

&lt;p&gt;Compact has a real type system, and it handles a lot of that automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Circom-style thinking: everything is a Field
signal input age;         // could be anything
signal input nonce;       // could be anything
signal output hash;       // field element

// Compact-style: types carry meaning built into the language
circuit example(age: Uint&amp;lt;8&amp;gt;, nonce: Bytes&amp;lt;32&amp;gt;): Bytes&amp;lt;32&amp;gt; {
  // age is already constrained to 0-255 by the type
  // nonce is already exactly 32 bytes
  return persistentHash&amp;lt;[Uint&amp;lt;8&amp;gt;, Bytes&amp;lt;32&amp;gt;]&amp;gt;([age, nonce]);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compact's built-in types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Boolean&lt;/code&gt; — true/false&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Field&lt;/code&gt; — unsigned integer up to the native prime (for raw field arithmetic)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Uint&amp;lt;n&amp;gt;&lt;/code&gt; — n-bit unsigned integer (&lt;code&gt;Uint&amp;lt;32&amp;gt;&lt;/code&gt;, &lt;code&gt;Uint&amp;lt;64&amp;gt;&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Uint&amp;lt;0..n&amp;gt;&lt;/code&gt; — bounded integer, guaranteed to be less than n&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Bytes&amp;lt;n&amp;gt;&lt;/code&gt; — fixed-length byte array of exactly n bytes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Vector&amp;lt;n, T&amp;gt;&lt;/code&gt; — homogeneous fixed-length tuple&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also define structs and enums, which Circom has no equivalent for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;struct MerkleEntry {
  left: Bytes&amp;lt;32&amp;gt;;
  right: Bytes&amp;lt;32&amp;gt;;
}

enum AccessLevel { NONE, READ, WRITE, ADMIN }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Templates → circuits
&lt;/h3&gt;

&lt;p&gt;Circom templates are parametric blueprints you instantiate into components. Compact circuits work the same way conceptually, but with a distinction Circom doesn't have: they're either &lt;code&gt;pure&lt;/code&gt; or impure.&lt;/p&gt;

&lt;p&gt;A pure circuit takes inputs, runs computation, returns outputs — no side effects, no ledger access, no witness calls. It's the direct equivalent of a Circom template used as a stateless function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export pure circuit hashPair(left: Bytes&amp;lt;32&amp;gt;, right: 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;([left, right]);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An impure circuit can read from and write to the on-chain ledger, and call witnesses:&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 castVote(choice: Bytes&amp;lt;32&amp;gt;): [] {
  assert(state == VoteState.OPEN, "Voting is closed");
  const voter = publicKey(localSecretKey(), epoch as Field as Bytes&amp;lt;32&amp;gt;);
  votes.insert(disclose(voter), disclose(choice));
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;export&lt;/code&gt; modifier makes a circuit callable from outside the contract. Your DApp TypeScript calls exported circuits to construct transactions. Unexported circuits are internal helpers — like non-&lt;code&gt;main&lt;/code&gt; templates in Circom.&lt;/p&gt;

&lt;p&gt;Generic circuits work too, with type and numeric parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export pure circuit hashVector&amp;lt;T, #N&amp;gt;(values: Vector&amp;lt;N, T&amp;gt;): Bytes&amp;lt;32&amp;gt; {
  return persistentHash&amp;lt;Vector&amp;lt;N, T&amp;gt;&amp;gt;(values);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Components → circuit calls and witnesses
&lt;/h3&gt;

&lt;p&gt;In Circom, a &lt;code&gt;component&lt;/code&gt; instantiates a sub-template and wires its signals into your circuit. That's how you compose logic — building a constraint graph one node at a time.&lt;/p&gt;

&lt;p&gt;In Compact, calling another circuit is just a function call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export pure circuit hashPair(a: Bytes&amp;lt;32&amp;gt;, b: 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;([a, b]);
}

export pure circuit hashTriple(a: Bytes&amp;lt;32&amp;gt;, b: Bytes&amp;lt;32&amp;gt;, c: Bytes&amp;lt;32&amp;gt;): Bytes&amp;lt;32&amp;gt; {
  const ab = hashPair(a, b);
  return hashPair(ab, c);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No wiring. No signal arrays. No component instantiation syntax.&lt;/p&gt;

&lt;p&gt;But there's a second kind of external input in Compact that Circom has no parallel for: &lt;strong&gt;witnesses&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A witness is a TypeScript or JavaScript function that runs off-chain, inside the user's DApp, and supplies private data into the circuit during proof generation. It's how private state enters a Compact contract — the circuit declares what it needs, and the witness provides it without putting anything in any public record.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Compact side: declare the witness signature
witness localSecretKey(): Bytes&amp;lt;32&amp;gt;;

export circuit authenticate(): [] {
  const sk = localSecretKey();    // private, called during proof generation
  const pk = publicKey(sk, nonce as Field as Bytes&amp;lt;32&amp;gt;);
  assert(owner == pk, "Not the registered owner");
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// TypeScript side: implement the witness in your DApp&lt;/span&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="na"&gt;localSecretKey&lt;/span&gt;&lt;span class="p"&gt;:&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="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;PrivateState&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;PrivateState&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="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="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="nx"&gt;privateState&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Private keys, secret notes, and local state all live in witnesses. When you ask "where does private input come from in Compact?" — it comes from here.&lt;/p&gt;




&lt;h3&gt;
  
  
  R1CS constraints → &lt;code&gt;assert()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Circom's &lt;code&gt;===&lt;/code&gt; operator assigns a value and adds an R1CS constraint simultaneously. &lt;code&gt;&amp;lt;==&lt;/code&gt; and &lt;code&gt;==&amp;gt;&lt;/code&gt; do the same in one direction. The circuit only produces a valid proof if all constraints are satisfied.&lt;/p&gt;

&lt;p&gt;Compact uses &lt;code&gt;assert()&lt;/code&gt; for the same job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Circom
signal input a;
signal input b;
signal output c;
c &amp;lt;== a * b;
a * b === c;
&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;// Compact
export pure circuit multiply(a: Field, b: Field): Field {
  const c = a * b;
  assert(c != 0, "Product must be non-zero");
  return c;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same semantics — a failing assertion means no valid proof. The difference is that &lt;code&gt;assert()&lt;/code&gt; reads like normal application code, which makes auditing circuit logic much less painful.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;code&gt;disclose()&lt;/code&gt; — the concept Circom doesn't have
&lt;/h3&gt;

&lt;p&gt;This one has no Circom equivalent, and it will confuse you until it clicks.&lt;/p&gt;

&lt;p&gt;In Compact, all data flowing into or from witnesses is treated as potentially private by default. The compiler tracks the "taint" of that data through your entire circuit. If any potentially-private value is about to be stored in the public ledger, returned from an exported circuit, or passed to another contract, you must explicitly wrap it in &lt;code&gt;disclose()&lt;/code&gt;. This applies to witness-derived values and — perhaps surprisingly — to exported circuit parameters too, since the compiler can't statically guarantee their origin.&lt;/p&gt;

&lt;p&gt;Think of it as a compiler-enforced consent form. Before private information goes public, you have to say so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;witness localSecretKey(): Bytes&amp;lt;32&amp;gt;;

export circuit register(): [] {
  const sk = localSecretKey();       // private
  const pk = publicKey(sk, nonce);   // still private (derived from sk)

  // This fails — storing private-derived data without disclosing:
  // owner = pk;

  // This is correct — explicitly declaring that pk goes on-chain:
  owner = disclose(pk);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see &lt;code&gt;disclose()&lt;/code&gt; everywhere in Compact code. It's not boilerplate — it's a design choice that catches accidental data leaks at compile time, before a circuit ever runs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The ledger: what Circom doesn't have
&lt;/h2&gt;

&lt;p&gt;The biggest structural difference between Circom and Compact is that Compact has on-chain state.&lt;/p&gt;

&lt;p&gt;In a Circom-based system, your ZK circuit is stateless. It takes inputs, produces a proof, and your Solidity contract handles the rest: storing commitments, enforcing access control, tracking balances. The circuit and the state machine are separate things you wire together yourself.&lt;/p&gt;

&lt;p&gt;In Compact, they live in the same file.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;ledger&lt;/strong&gt; is Compact's on-chain state machine. It holds values that persist across every transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export ledger state: VoteState;
export ledger owner: Bytes&amp;lt;32&amp;gt;;
export ledger totalVotes: Counter;
export ledger balances: Map&amp;lt;Bytes&amp;lt;32&amp;gt;, Uint&amp;lt;64&amp;gt;&amp;gt;;
export ledger approvedSet: Set&amp;lt;Bytes&amp;lt;32&amp;gt;&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ledger values are public — everyone can read them. Privacy comes from keeping inputs private (in witnesses) and using ZK proofs to verify computations without revealing those inputs.&lt;/p&gt;

&lt;p&gt;Ledger types go well beyond primitives:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ledger type&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Counter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Increment/decrement with overflow protection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cell&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Wraps any regular type with read/write/reset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Map&amp;lt;K, V&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Key-value store, useful for allowlists or balances&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Set&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unordered membership collection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MerkleTree&amp;lt;n, T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;On-chain Merkle tree with depth n&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HistoricMerkleTree&amp;lt;n, T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Versioned Merkle tree for past root verification&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The constructor initializes ledger state at deployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;constructor() {
  state = VoteState.PENDING;
  totalVotes.increment(1);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ZK proof logic and state machine in one language. The mental model shifts from "write a circuit and bolt it onto a contract" to "write a contract where privacy is built in from the start."&lt;/p&gt;




&lt;h2&gt;
  
  
  Side-by-side: range proof
&lt;/h2&gt;

&lt;p&gt;A range proof shows that a private value falls within a given range without revealing the value itself. Good place to start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In Circom:&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;pragma circom 2.0.0;
include "circomlib/circuits/comparators.circom";

template AgeRangeProof(bits) {
    signal input age;     // private input
    signal output valid;

    component upperBound = LessThan(bits);
    upperBound.in[0] &amp;lt;== age;
    upperBound.in[1] &amp;lt;== 120;

    component lowerBound = GreaterEqThan(bits);
    lowerBound.in[0] &amp;lt;== age;
    lowerBound.in[1] &amp;lt;== 18;

    // Both conditions must hold
    valid &amp;lt;== upperBound.out * lowerBound.out;
}

component main = AgeRangeProof(8);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;In Compact:&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;pragma language_version &amp;gt;= 0.22;

witness privateAge(): Uint&amp;lt;8&amp;gt;;

export circuit proveAgeRange(): [] {
    const age = privateAge();
    assert(age &amp;gt;= 18, "Must be at least 18 years old");
    assert(age &amp;lt; 120, "Age exceeds expected maximum");
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Uint&amp;lt;8&amp;gt;&lt;/code&gt; already constrains age to 0-255 by the type, so no bit decomposition needed. The &lt;code&gt;LessThan&lt;/code&gt; and &lt;code&gt;GreaterEqThan&lt;/code&gt; component wiring becomes two &lt;code&gt;assert()&lt;/code&gt; comparisons. The private input comes through a &lt;code&gt;witness&lt;/code&gt; instead of a &lt;code&gt;signal input&lt;/code&gt;. There's no output signal to wire up — a failing &lt;code&gt;assert&lt;/code&gt; means no valid proof, which is the constraint.&lt;/p&gt;




&lt;h2&gt;
  
  
  Side-by-side: Merkle membership proof
&lt;/h2&gt;

&lt;p&gt;Merkle membership proofs show up everywhere in ZK systems: prove a private leaf exists in a public tree without revealing which leaf.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In Circom:&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;pragma circom 2.0.0;
include "circomlib/circuits/poseidon.circom";
include "circomlib/circuits/mux1.circom";

template MerkleProof(depth) {
    signal input leaf;                    // private
    signal input root;                    // public
    signal input pathElements[depth];     // private sibling hashes
    signal input pathIndices[depth];      // 0=left, 1=right

    component hashers[depth];
    component selectors[depth];
    signal levelHash[depth + 1];

    levelHash[0] &amp;lt;== leaf;

    for (var i = 0; i &amp;lt; depth; i++) {
        selectors[i] = MultiMux1(2);
        selectors[i].c[0][0] &amp;lt;== levelHash[i];
        selectors[i].c[0][1] &amp;lt;== pathElements[i];
        selectors[i].c[1][0] &amp;lt;== pathElements[i];
        selectors[i].c[1][1] &amp;lt;== levelHash[i];
        selectors[i].s &amp;lt;== pathIndices[i];

        hashers[i] = Poseidon(2);
        hashers[i].inputs[0] &amp;lt;== selectors[i].out[0];
        hashers[i].inputs[1] &amp;lt;== selectors[i].out[1];

        levelHash[i + 1] &amp;lt;== hashers[i].out;
    }

    root === levelHash[depth];
}

component main { public [root] } = MerkleProof(20);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;In Compact:&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;pragma language_version &amp;gt;= 0.22;
import CompactStandardLibrary;

// The Merkle root lives on-chain, publicly visible
export ledger treeRoot: Field;

// The full membership path stays private — provided off-chain by the witness
witness getMembershipPath(): MerkleTreePath&amp;lt;20, Bytes&amp;lt;32&amp;gt;&amp;gt;;

// Prove a private leaf exists in the committed tree
export circuit proveMembership(): [] {
    const path = getMembershipPath();
    const computed = merkleTreePathRoot&amp;lt;20, Bytes&amp;lt;32&amp;gt;&amp;gt;(path);
    assert(
        computed.field == treeRoot,
        "Proof failed: leaf is not in the committed Merkle tree"
    );
}

// Update the committed root (admin operation)
// disclose() required: exported circuit params are treated as
// potentially witness-tainted when writing to ledger
export circuit updateRoot(newRoot: Field): [] {
    treeRoot = disclose(newRoot);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Off-chain witness in TypeScript:&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PrivateState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;leaf&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="nl"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;sibling&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;field&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="nl"&gt;goesLeft&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&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="p"&gt;};&lt;/span&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="na"&gt;getMembershipPath&lt;/span&gt;&lt;span class="p"&gt;:&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="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;PrivateState&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;PrivateState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MerkleTreePath&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="k"&gt;return&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="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;leaf&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="nx"&gt;leaf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;path&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="nx"&gt;path&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="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 Circom version is roughly 40 lines of wiring: manual left/right selection at each level, explicit loop over depth, individual component instantiation per level. The Compact version is 8 lines of contract logic. &lt;code&gt;merkleTreePathRoot&lt;/code&gt; handles path traversal and hashing internally — it's a standard library circuit, not something you wire yourself.&lt;/p&gt;

&lt;p&gt;In Circom, private inputs are individual signals: &lt;code&gt;pathElements[depth]&lt;/code&gt;, &lt;code&gt;pathIndices[depth]&lt;/code&gt;. In Compact, they're a single typed struct (&lt;code&gt;MerkleTreePath&amp;lt;20, Bytes&amp;lt;32&amp;gt;&amp;gt;&lt;/code&gt;) with sibling hashes and direction flags bundled together, supplied entirely by the TypeScript witness.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;MerkleTreePath&lt;/code&gt; never appears in any on-chain data. It exists only during proof generation. The only thing that touches the ledger is the root, explicitly stored via &lt;code&gt;disclose(newRoot)&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common pitfalls for Circom developers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Forgetting &lt;code&gt;disclose()&lt;/code&gt; when storing to ledger
&lt;/h3&gt;

&lt;p&gt;The compiler error looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;potential witness-value disclosure must be declared but is not
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule is wider than most people expect: &lt;strong&gt;any value stored in the ledger from an exported circuit requires &lt;code&gt;disclose()&lt;/code&gt;&lt;/strong&gt; — not just witness data. This includes plain circuit parameters. The compiler treats exported circuit arguments as potentially witness-tainted because, in the ZK proof model, their origin can't be statically guaranteed. The fix is straightforward: wrap the value at the point of ledger assignment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Fails — even a plain circuit parameter needs disclose() before hitting the ledger
export circuit updateRoot(newRoot: Field): [] {
    treeRoot = newRoot;  // Error: potential witness-value disclosure
}

// Correct
export circuit updateRoot(newRoot: Field): [] {
    treeRoot = disclose(newRoot);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put &lt;code&gt;disclose()&lt;/code&gt; as close to the disclosure point as possible — not wrapped around entire expression chains.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Using &lt;code&gt;transientHash&lt;/code&gt; for ledger-stored commitments
&lt;/h3&gt;

&lt;p&gt;Compact has two hash functions: &lt;code&gt;transientHash&lt;/code&gt; (optimized for circuit performance, outputs &lt;code&gt;Field&lt;/code&gt;) and &lt;code&gt;persistentHash&lt;/code&gt; (SHA-256 based, outputs &lt;code&gt;Bytes&amp;lt;32&amp;gt;&lt;/code&gt;). For any value stored in the ledger, use &lt;code&gt;persistentHash&lt;/code&gt;. If you use &lt;code&gt;transientHash&lt;/code&gt; for a commitment and the contract gets upgraded, the hash output may change and invalidate all existing proofs against old commitments. &lt;code&gt;persistentHash&lt;/code&gt; is stable across contract upgrades by design.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Expecting Circom's loop patterns to transfer directly
&lt;/h3&gt;

&lt;p&gt;Compact loops follow the same bounded-compile-time rule Circom does, but the syntax is different and the type system enforces it more aggressively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Valid: bounded by a constant
for (const i of 0..10) { ... }

// Valid: bounded by vector size
for (const elem of myVector) { ... }

// Invalid: no dynamic upper bounds, no recursion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No recursion in Compact. Every circuit must have a provably finite execution path, and the compiler won't let you accidentally create one that doesn't.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Treating witnesses like Circom components
&lt;/h3&gt;

&lt;p&gt;Witnesses are TypeScript. They run outside the circuit entirely. You can't add constraints inside a witness, and the Compact docs specifically note that you should not assume the witness executes your code exactly as written. Constraints belong in &lt;code&gt;assert()&lt;/code&gt; inside circuits — not in witness logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Forgetting &lt;code&gt;pure&lt;/code&gt; on utility circuits
&lt;/h3&gt;

&lt;p&gt;If a circuit doesn't access the ledger or call witnesses, mark it &lt;code&gt;pure&lt;/code&gt;. It's not just a style preference — pure circuits can be called in more contexts (including from other pure circuits) and make it immediately obvious when something unexpectedly touches state. An accidental ledger access in a utility circuit is much easier to catch as a compiler error than as a runtime bug.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Assuming R1CS tooling transfers
&lt;/h3&gt;

&lt;p&gt;Groth16 ceremonies, snarkjs scripts, R1CS export workflows — none of it applies in Compact. Compact compiles to ZKIR. You'll use the Midnight SDK toolchain for compilation, proof generation, and DApp integration. Plan for that as a separate learning track, not an afternoon of config changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  What carries over
&lt;/h2&gt;

&lt;p&gt;A lot, actually.&lt;/p&gt;

&lt;p&gt;The constraint-first mindset — "if this condition fails, there's no valid proof" — maps directly onto &lt;code&gt;assert()&lt;/code&gt;. Private inputs never leaving the circuit becomes witness isolation. Value commitments become &lt;code&gt;persistentHash&lt;/code&gt; plus &lt;code&gt;persistentCommit&lt;/code&gt;. Your understanding of Merkle proofs, nullifiers, and commitment schemes applies directly — the standard library has all of those primitives.&lt;/p&gt;

&lt;p&gt;The scope is what changes more than the syntax. Circom asked you to think about one circuit. Compact asks you to think about a full contract: state machine, multiple entry points, privacy logic woven through all of it. Once that framing settles in, the translation starts feeling less like learning a new language and more like writing familiar logic in a bigger room.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You want to...&lt;/th&gt;
&lt;th&gt;In Circom&lt;/th&gt;
&lt;th&gt;In Compact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Define a reusable circuit&lt;/td&gt;
&lt;td&gt;&lt;code&gt;template Foo() { }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;circuit foo(): T { }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expose a circuit to callers&lt;/td&gt;
&lt;td&gt;&lt;code&gt;component main = Foo()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;export circuit foo(): T { }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add a constraint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;a === b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;assert(a == b, "msg")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hash two values&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Poseidon(2)&lt;/code&gt; component&lt;/td&gt;
&lt;td&gt;&lt;code&gt;persistentHash&amp;lt;Vector&amp;lt;2, T&amp;gt;&amp;gt;([a, b])&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supply private data&lt;/td&gt;
&lt;td&gt;&lt;code&gt;signal input x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;witness getX(): T&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Store on-chain state&lt;/td&gt;
&lt;td&gt;(Solidity contract)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ledger val: T&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Declare privacy intent&lt;/td&gt;
&lt;td&gt;(not required)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;disclose(value)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verify Merkle membership&lt;/td&gt;
&lt;td&gt;manual component wiring&lt;/td&gt;
&lt;td&gt;&lt;code&gt;merkleTreePathRoot&amp;lt;n, T&amp;gt;(path)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.midnight.network/develop/reference/compact/lang-ref" rel="noopener noreferrer"&gt;Compact Language Reference&lt;/a&gt; — full syntax spec&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.midnight.network/blog/compact" rel="noopener noreferrer"&gt;Compact Deep Dive Part 1&lt;/a&gt; — contract structure walkthrough&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.midnight.network/blog/compact-2" rel="noopener noreferrer"&gt;Compact Deep Dive Part 2&lt;/a&gt; — circuits and witnesses in depth&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.midnight.network/tutorials/bboard/smart-contract" rel="noopener noreferrer"&gt;Bulletin Board Tutorial&lt;/a&gt; — the reference contract that uses every concept above in one small file&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.midnight.network/compact/standard-library/exports" rel="noopener noreferrer"&gt;Standard Library Exports&lt;/a&gt; — all built-in types and circuit signatures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're coming from Circom, read the bulletin board contract first (&lt;code&gt;bboard.compact&lt;/code&gt;). It's small, uses a witness, a persistent hash commitment, a ledger state machine, and &lt;code&gt;disclose()&lt;/code&gt; all together. Everything in this guide shows up there.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;All Compact examples in this article were compiled and verified against Compact compiler v0.31.0. Check the &lt;a href="https://docs.midnight.network/relnotes/compact" rel="noopener noreferrer"&gt;compiler release notes&lt;/a&gt; for the current version before you start.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>compact</category>
      <category>webdev</category>
      <category>midnightfordevs</category>
    </item>
    <item>
      <title>Compact Standard Library: A Verified Reference to Every Export</title>
      <dc:creator>Harrie</dc:creator>
      <pubDate>Mon, 04 May 2026 17:33:00 +0000</pubDate>
      <link>https://dev.to/iamharrie/compact-standard-library-a-verified-reference-to-every-export-10k5</link>
      <guid>https://dev.to/iamharrie/compact-standard-library-a-verified-reference-to-every-export-10k5</guid>
      <description>&lt;p&gt;I got frustrated trying to use the Compact standard library.&lt;/p&gt;

&lt;p&gt;The docs list types like &lt;code&gt;CurvePoint&lt;/code&gt;, &lt;code&gt;Scalar&lt;/code&gt;, &lt;code&gt;MerkleTree&lt;/code&gt; as if they work. Other articles repeat those names. You copy the pattern, run the compiler, and get an error telling you the name was deprecated two versions ago. Or that the function signature is wrong. Or that the type simply doesn't exist.&lt;/p&gt;

&lt;p&gt;So I ran every single export through the compiler. Not "tested the general concept" — ran actual contracts through Compact v0.30.0 until they compiled or didn't. What follows is what I found: exact signatures, working code, and a clear note wherever something that's documented doesn't actually work yet.&lt;/p&gt;

&lt;p&gt;Every code block in this article compiles. Where something doesn't, that's stated explicitly, with the error.&lt;/p&gt;

&lt;p&gt;The companion repo — &lt;a href="https://github.com/IamHarrie-Labs/compact-stdlib-reference" rel="noopener noreferrer"&gt;github.com/IamHarrie-Labs/compact-stdlib-reference&lt;/a&gt; — has all seven contracts as standalone &lt;code&gt;.compact&lt;/code&gt; files you can run yourself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Before you write a single line: the ZK mental model
&lt;/h2&gt;

&lt;p&gt;Compact contracts don't run like normal programs. A circuit is a mathematical constraint system, not a sequence of instructions. When you call &lt;code&gt;rotateNonce()&lt;/code&gt;, the compiler doesn't generate code that &lt;em&gt;runs&lt;/em&gt; — it generates a zero-knowledge proof that the output was derived correctly from the inputs, without revealing the private inputs themselves.&lt;/p&gt;

&lt;p&gt;This matters for two things that catch people off guard:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State branching is constrained.&lt;/strong&gt; You can't write &lt;code&gt;if (stored == none) { ... }&lt;/code&gt; and have different circuit paths execute at runtime. The circuit structure is fixed at compile time. That's why &lt;code&gt;Maybe&amp;lt;T&amp;gt;&lt;/code&gt; has no &lt;code&gt;is some&lt;/code&gt; operator — conditional branching on optional presence would require a non-deterministic circuit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy is explicit.&lt;/strong&gt; Compact distinguishes between &lt;em&gt;witness values&lt;/em&gt; (private, only the prover knows them) and &lt;em&gt;public values&lt;/em&gt; (on the ledger, visible to everyone). The compiler refuses to let you write a witness value to public state without explicitly calling &lt;code&gt;disclose()&lt;/code&gt;. This isn't boilerplate — it's the compiler enforcing the privacy model. If you try to skip it, you get a compilation error, not a runtime error.&lt;/p&gt;

&lt;p&gt;Keep these two things in mind and most of the API makes sense immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to import
&lt;/h2&gt;

&lt;p&gt;Every contract that uses the standard library starts with:&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;pragma&lt;/span&gt; &lt;span class="nx"&gt;language_version&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;CompactStandardLibrary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One import, all ~30 exports in scope. You don't need individual imports per type.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Maybe&amp;lt;T&amp;gt; — optional values
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Maybe&amp;lt;T&amp;gt;&lt;/code&gt; represents a value that may or may not exist. Use it for optional ledger fields, nullable state, and anything that gets set after construction.&lt;/p&gt;

&lt;p&gt;There are two constructors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;none&amp;lt;T&amp;gt;()&lt;/code&gt; — the absent case&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;some&amp;lt;T&amp;gt;(value)&lt;/code&gt; — the present case&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And one accessor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.value&lt;/code&gt; — retrieves the inner value&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important thing about &lt;code&gt;.value&lt;/code&gt;: if you call it on a &lt;code&gt;none&lt;/code&gt;, the ZK proof cannot be constructed. No exception is thrown, no runtime panic — proof generation simply fails. This is consistent with how circuits work. The constraint that "this field contains a value" is either satisfiable or it isn't.&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;pragma&lt;/span&gt; &lt;span class="nx"&gt;language_version&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;CompactStandardLibrary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Maybe&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&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="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;none&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&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="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;setStored&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;stored&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="nx"&gt;some&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&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="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;clearStored&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="nx"&gt;stored&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="nx"&gt;none&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&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="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;useStored&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&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;// If stored is none, proof generation fails here — not a runtime exception&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&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;There is no &lt;code&gt;is some&lt;/code&gt; or &lt;code&gt;is none&lt;/code&gt; keyword in Compact. You can't branch on whether a &lt;code&gt;Maybe&lt;/code&gt; is present inside a circuit. The right design: use separate circuits. Have one circuit that runs when the field is guaranteed to contain a value (&lt;code&gt;useStored&lt;/code&gt;) and a different one to set it (&lt;code&gt;setStored&lt;/code&gt;). The application layer decides which to call based on ledger state it reads off-chain.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Either&amp;lt;L, R&amp;gt; — two-branch union
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Either&amp;lt;L, R&amp;gt;&lt;/code&gt; holds exactly one of two typed values. The standard library uses it in a few places internally — &lt;code&gt;shieldedBurnAddress()&lt;/code&gt; returns &lt;code&gt;Either&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;&lt;/code&gt;, and the OpenZeppelin &lt;code&gt;Ownable.compact&lt;/code&gt; stores the owner as &lt;code&gt;Either&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;&lt;/code&gt; to allow both user-key and contract-address ownership.&lt;/p&gt;

&lt;p&gt;Constructors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;left&amp;lt;L, R&amp;gt;(value)&lt;/code&gt; — left branch&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;right&amp;lt;L, R&amp;gt;(value)&lt;/code&gt; — right branch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Accessors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.left&lt;/code&gt; — retrieves the left value; proof fails if this is a right value&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.right&lt;/code&gt; — retrieves the right value; proof fails if this is a left value
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;pragma&lt;/span&gt; &lt;span class="nx"&gt;language_version&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;CompactStandardLibrary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Either&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&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="nx"&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="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;choice&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="nx"&gt;left&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&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="nx"&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="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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;initial&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;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;pickLeft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;choice&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="nx"&gt;left&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&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="nx"&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="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;pickRight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;choice&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="nx"&gt;right&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&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="nx"&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="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;readLeft&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&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;// Proof fails if choice is currently right — not a runtime exception&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&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;Accessing &lt;code&gt;.left&lt;/code&gt; on a right value means the proof can't be constructed — same failure mode as &lt;code&gt;.value&lt;/code&gt; on &lt;code&gt;none&lt;/code&gt;. No branching on which side is active inside a circuit. The application layer reads the ledger state and calls the right circuit.&lt;/p&gt;

&lt;p&gt;By convention, &lt;code&gt;left&lt;/code&gt; tends to hold the "primary" or "success" case and &lt;code&gt;right&lt;/code&gt; the "alternative" — but Compact doesn't enforce this. OpenZeppelin's &lt;code&gt;Ownable.compact&lt;/code&gt; uses it as &lt;code&gt;Either&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;&lt;/code&gt; where left is a user key and right is a contract address, which is probably the most common real-world usage.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Counter — monotonic sequence numbers
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Counter&lt;/code&gt; is a ledger-only type that increments. It's the canonical way to track sequence numbers, nonces, and usage counts in Compact.&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;pragma&lt;/span&gt; &lt;span class="nx"&gt;language_version&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;CompactStandardLibrary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;lastKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;witness&lt;/span&gt; &lt;span class="nf"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&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="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;increment&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="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;nextKey&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;// Use the counter value as part of a domain-separated hash&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;persistentHash&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&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="nx"&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="nx"&gt;seq&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Field&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&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="nf"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="nx"&gt;lastKey&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="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;increment&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;seq.increment(n)&lt;/code&gt; mutates the ledger field in place. There's no assignment syntax — you don't write &lt;code&gt;seq = seq + 1&lt;/code&gt;. The mutation is applied directly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;seq as Field&lt;/code&gt; casts the counter to a &lt;code&gt;Field&lt;/code&gt; value you can feed into arithmetic or hash inputs. The double cast &lt;code&gt;seq as Field as Bytes&amp;lt;32&amp;gt;&lt;/code&gt; above handles the type coercion needed for vector input.&lt;/p&gt;

&lt;p&gt;Counters can't be decremented. A ZK circuit that allowed decrement would need to prove the new value is less than the old one, which changes the circuit structure. So Compact just doesn't allow it: counters go up, never down.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Hashing and commitments
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;persistentHash&lt;/code&gt; — domain-separated one-way hash
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;persistentHash&lt;/code&gt; is the primary hash function in Compact. Commitments, access control checks, nullifiers — if you're doing any kind of cryptographic binding in a circuit, you're using this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;persistentHash&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Vector&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&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;Bytes&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Vector&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&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;Bytes&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;):&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The type parameter tells the compiler how many inputs you're providing. You get back a 32-byte hash.&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;pragma&lt;/span&gt; &lt;span class="nx"&gt;language_version&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;CompactStandardLibrary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;commitment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;witness&lt;/span&gt; &lt;span class="nf"&gt;secretValue&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&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;export&lt;/span&gt; &lt;span class="nx"&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="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;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;persistentHash&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&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="nx"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;myapp:commit:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;secretValue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="nx"&gt;commitment&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="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;verify&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;persistentHash&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&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="nx"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;myapp:commit:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;secretValue&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="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nx"&gt;commitment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Proof invalid: secret does not match commitment&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without a domain prefix, the same secret value hashes to the same output in every circuit — which means a proof generated for one circuit can potentially satisfy constraints in a different one. Adding a domain string like &lt;code&gt;"myapp:commit:"&lt;/code&gt; ties each hash to its specific application. The &lt;code&gt;example-bboard&lt;/code&gt; canonical reference uses &lt;code&gt;"bboard:pk:"&lt;/code&gt; as its domain separator. Pick something specific to your contract and use it consistently.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;code&gt;pad&lt;/code&gt; — string to bytes
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;pad(n, str)&lt;/code&gt; converts a string literal to a &lt;code&gt;Bytes&amp;lt;n&amp;gt;&lt;/code&gt; value, right-padded with zeros. Almost always used to create domain separator inputs for &lt;code&gt;persistentHash&lt;/code&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;pragma&lt;/span&gt; &lt;span class="nx"&gt;language_version&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;CompactStandardLibrary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;hashDomain&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="nx"&gt;h&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="nx"&gt;persistentHash&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;myapp:hash:&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;n&lt;/code&gt; must match the target type exactly. &lt;code&gt;pad(32, "myapp:")&lt;/code&gt; produces &lt;code&gt;Bytes&amp;lt;32&amp;gt;&lt;/code&gt;. If you use &lt;code&gt;pad(16, "myapp:")&lt;/code&gt; and the target is &lt;code&gt;Bytes&amp;lt;32&amp;gt;&lt;/code&gt;, you'll get a type error.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;code&gt;persistentCommit&lt;/code&gt; — append-only commitment accumulator
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;persistentCommit&lt;/code&gt; advances a commitment root by incorporating a new value. Think of it as a Merkle accumulator in a single function call: you have a root, you add a value, you get a new root that commits to everything that's been added so far.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;persistentCommit&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;):&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;&amp;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 typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;pragma&lt;/span&gt; &lt;span class="nx"&gt;language_version&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;CompactStandardLibrary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&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="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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;empty&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;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;root&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="nx"&gt;persistentCommit&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&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="nx"&gt;root&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="nx"&gt;value&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 root represents the accumulated state of all values appended so far. You can't remove values or reorder them — the accumulator is append-only, which is exactly what you want for an audit log or a history of actions.&lt;/p&gt;

&lt;p&gt;One more thing on MerkleTree: the type &lt;code&gt;MerkleTree&amp;lt;N, T&amp;gt;&lt;/code&gt; exists in the type system and appears in documentation, but you cannot store it directly as a ledger field. The compiler rejects it as an ADT type in ledger position. The error looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error: algebraic data types not supported in ledger fields
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need accumulator functionality — which is what MerkleTree gives you in most use cases — &lt;code&gt;persistentCommit&lt;/code&gt; with a &lt;code&gt;Bytes&amp;lt;32&amp;gt;&lt;/code&gt; root is exactly what you want. The root is the Merkle accumulator state; &lt;code&gt;persistentCommit&lt;/code&gt; is the operation that advances it.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Kernel identity types
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;ZswapCoinPublicKey&lt;/code&gt; — user public keys
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ZswapCoinPublicKey&lt;/code&gt; is the type returned by &lt;code&gt;ownPublicKey()&lt;/code&gt; and used to represent a user's public key on Midnight. It shows up in ownership patterns, access control, and the &lt;code&gt;shieldedBurnAddress()&lt;/code&gt; return type.&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;pragma&lt;/span&gt; &lt;span class="nx"&gt;language_version&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;CompactStandardLibrary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;registered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ZswapCoinPublicKey&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;register&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="nx"&gt;registered&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;ownPublicKey&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;There's a significant security problem with this pattern. &lt;code&gt;ownPublicKey()&lt;/code&gt; compiles to an unconstrained &lt;code&gt;private_input&lt;/code&gt; — the prover supplies the value without any proof of ownership. Anyone who reads &lt;code&gt;registered&lt;/code&gt; from the ledger can call &lt;code&gt;withdraw()&lt;/code&gt; with that same value in their &lt;code&gt;CircuitContext&lt;/code&gt;. They don't need the secret key. The circuit has no way to distinguish the real key holder from someone who just copied the stored value.&lt;/p&gt;

&lt;p&gt;The fix is to store a commitment instead:&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="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;vault_owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;witness&lt;/span&gt; &lt;span class="nf"&gt;localSecretKey&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;ownerCommitment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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="nx"&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="nx"&gt;persistentHash&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&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="nx"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vault:owner:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;deposit&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="nx"&gt;vault_owner&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;ownerCommitment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;localSecretKey&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;withdraw&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="nf"&gt;ownerCommitment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;localSecretKey&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nx"&gt;vault_owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Not the vault owner&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the contract stores a hash of the secret key, not the key itself. The &lt;code&gt;withdraw&lt;/code&gt; circuit requires the prover to supply the actual secret key (which they keep private), and proves that its hash matches what's on the ledger. An attacker who copies &lt;code&gt;vault_owner&lt;/code&gt; from the ledger can't construct a valid proof without the secret key. This is covered in detail in the companion article on the &lt;code&gt;ownPublicKey()&lt;/code&gt; vulnerability.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;code&gt;ContractAddress&lt;/code&gt; — contract identity
&lt;/h3&gt;

&lt;p&gt;Represents the on-chain address of a deployed contract. Used in token type derivation and any ownership pattern where a contract, rather than a user, needs to be the owner.&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;pragma&lt;/span&gt; &lt;span class="nx"&gt;language_version&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;CompactStandardLibrary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;nativeT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;customT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;setTokenTypes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ContractAddress&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="nx"&gt;nativeT&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;nativeToken&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="nx"&gt;customT&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;tokenType&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;token:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;addr&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;h3&gt;
  
  
  &lt;code&gt;UserAddress&lt;/code&gt; — cross-layer user identity
&lt;/h3&gt;

&lt;p&gt;A higher-level user identity type encoding network-specific address formats. Used when interfacing with Midnight's identity or wallet layers rather than the ZK proof system directly. It's in scope from &lt;code&gt;import CompactStandardLibrary;&lt;/code&gt; but doesn't appear often in typical contract code.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Helper circuits
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;ownPublicKey&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;ownPublicKey&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;ZswapCoinPublicKey&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns the public key supplied by the prover in their &lt;code&gt;CircuitContext&lt;/code&gt;. Unconstrained — see the &lt;code&gt;ZswapCoinPublicKey&lt;/code&gt; section above for the security implications and the fix.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;code&gt;nativeToken&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;nativeToken&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns the token type identifier for Midnight's native NIGHT token. Use this when your contract needs to reference or compare against the native token.&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;pragma&lt;/span&gt; &lt;span class="nx"&gt;language_version&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;CompactStandardLibrary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;acceptNative&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="nx"&gt;accepted&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;nativeToken&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;h3&gt;
  
  
  &lt;code&gt;tokenType&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;tokenType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ContractAddress&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Derives a unique token type identifier for a given contract address with a domain prefix. Use this to create a token identifier tied to your contract.&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;pragma&lt;/span&gt; &lt;span class="nx"&gt;language_version&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;CompactStandardLibrary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;nativeT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;customT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;setTokenTypes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokenContract&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ContractAddress&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="nx"&gt;nativeT&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;nativeToken&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="nx"&gt;customT&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;tokenType&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;token:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;tokenContract&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 signature requires two arguments: a &lt;code&gt;Bytes&amp;lt;32&amp;gt;&lt;/code&gt; prefix and the &lt;code&gt;ContractAddress&lt;/code&gt;. Several community articles show &lt;code&gt;tokenType(contractAddress)&lt;/code&gt; with only one argument — the compiler rejects this. The prefix is required. Use &lt;code&gt;pad(32, "token:")&lt;/code&gt; or a more specific domain string.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;code&gt;evolveNonce&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;evolveNonce&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="n"&gt;Uint&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="m"&gt;128&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;):&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Derives a new nonce bytes value from a counter and a domain tag. The main use case is generating unique nullifiers or per-action commitment seeds — values that are different for every action, deterministically derived from a counter, and impossible to predict without knowing the counter state.&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;pragma&lt;/span&gt; &lt;span class="nx"&gt;language_version&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;CompactStandardLibrary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;128&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;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;nonceHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;nonce&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="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;128&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;nonceHash&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;initial:&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;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;rotateNonce&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="nx"&gt;nonceHash&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;evolveNonce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nonce&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rotate:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
  &lt;span class="nx"&gt;nonce&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="nx"&gt;nonce&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;128&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 first argument is &lt;code&gt;Uint&amp;lt;128&amp;gt;&lt;/code&gt;, not &lt;code&gt;Bytes&amp;lt;32&amp;gt;&lt;/code&gt;. This is the most common mistake with this function — other articles show &lt;code&gt;Bytes&amp;lt;32&amp;gt;&lt;/code&gt; as the first argument, and the compiler rejects it. The ledger field storing the nonce counter needs to be typed &lt;code&gt;Uint&amp;lt;128&amp;gt;&lt;/code&gt;, and the return type is &lt;code&gt;Bytes&amp;lt;32&amp;gt;&lt;/code&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;code&gt;shieldedBurnAddress&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;shieldedBurnAddress&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;Either&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ZswapCoinPublicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ContractAddress&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns the canonical burn address for permanently destroying shielded tokens. Sending tokens to this address is irrecoverable — nothing can spend from the burn address.&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;pragma&lt;/span&gt; &lt;span class="nx"&gt;language_version&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;CompactStandardLibrary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;burnAddr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Either&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ZswapCoinPublicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ContractAddress&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;burnAddr&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;shieldedBurnAddress&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;h3&gt;
  
  
  &lt;code&gt;disclose&lt;/code&gt; — making private values public
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;disclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&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;T&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;disclose&lt;/code&gt; is how Compact enforces the boundary between private and public state. Witness values — anything returned by a &lt;code&gt;witness&lt;/code&gt; function — are private: only the prover knows them. To write a witness-derived value to a ledger field, which is public, you must explicitly call &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 typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;pragma&lt;/span&gt; &lt;span class="nx"&gt;language_version&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;CompactStandardLibrary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;witness&lt;/span&gt; &lt;span class="nf"&gt;privateData&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&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;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;store&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;// Without disclose(), this produces a compile error:&lt;/span&gt;
  &lt;span class="c1"&gt;// "potential witness-value disclosure not wrapped in disclose()"&lt;/span&gt;
  &lt;span class="nx"&gt;stored&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;privateData&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 compiler will refuse to let you skip this. That's intentional. The requirement to explicitly type &lt;code&gt;disclose&lt;/code&gt; is a forcing function: you have to consciously decide, for every ledger write, "yes, I am choosing to make this value public." It catches accidental disclosure of private data at compile time rather than at runtime.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Elliptic curve: &lt;code&gt;JubjubPoint&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The elliptic curve type in Compact is &lt;code&gt;JubjubPoint&lt;/code&gt;. If you've read older documentation or articles that use &lt;code&gt;CurvePoint&lt;/code&gt;, that name has been deprecated. The compiler gives you a clear error when you try to use it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apparent use of an old standard-library name CurvePoint: the new name is JubjubPoint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;JubjubPoint&lt;/code&gt; is an opaque type. It has no accessible &lt;code&gt;.x&lt;/code&gt; or &lt;code&gt;.y&lt;/code&gt; fields. You can't add or multiply them directly. The compiler reports it as &lt;code&gt;Opaque&amp;lt;"JubjubPoint"&amp;gt;&lt;/code&gt;, which means it's a value your circuit can hold and pass around, but the internal structure is hidden. The Jubjub curve is used internally by the ZK proving system for things like Pedersen commitments and keypair operations — it's not a general-purpose EC type you'd use for application-level math.&lt;/p&gt;

&lt;p&gt;If you need commitments in your contract, use &lt;code&gt;persistentHash&lt;/code&gt;. It works, it's fully supported, and you don't need to touch curve points at all.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Scalar&lt;/code&gt; — the companion type used alongside &lt;code&gt;CurvePoint&lt;/code&gt; for scalar multiplication in older documentation — is also not available. It produces an "unbound identifier" error in v0.30.0. For scalar arithmetic in circuit code, use &lt;code&gt;Field&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Shielded token operations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;receiveShielded&lt;/code&gt; and &lt;code&gt;sendShielded&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;These handle private token transfers into and out of contracts. They operate on &lt;code&gt;ShieldedCoinInfo&lt;/code&gt; — structured coin data from Midnight's UTXO layer — and interact with the full shielded transaction pipeline.&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;pragma&lt;/span&gt; &lt;span class="nx"&gt;language_version&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;CompactStandardLibrary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;tokenType_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;witness&lt;/span&gt; &lt;span class="nf"&gt;inputCoin&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;ShieldedCoinInfo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;depositShielded&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="nx"&gt;tokenType_&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;nativeToken&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="nf"&gt;receiveShielded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;inputCoin&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;Shielded transfers need the full proof pipeline — proof server, shielded coin commitment tree, the whole thing. &lt;code&gt;skipZk: true&lt;/code&gt; won't catch the proof-level constraints; you need a running local Devnet. &lt;code&gt;sendShielded&lt;/code&gt; is the harder one — it requires the proof server to generate output proofs, and local Devnet setup is the only reliable way to test it end-to-end.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ShieldedCoinInfo&lt;/code&gt; is in scope from &lt;code&gt;import CompactStandardLibrary;&lt;/code&gt;. No separate import needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Block-time queries
&lt;/h2&gt;

&lt;p&gt;The bounty specification and a few community articles reference block-time functions: &lt;code&gt;getBlockTime()&lt;/code&gt;, &lt;code&gt;getBlockNumber()&lt;/code&gt;, &lt;code&gt;getEpoch()&lt;/code&gt;. As of compiler v0.30.0, none of these compile.&lt;/p&gt;

&lt;p&gt;I tried every reasonable naming variant: &lt;code&gt;blockHeight&lt;/code&gt;, &lt;code&gt;currentBlock&lt;/code&gt;, &lt;code&gt;currentBlockHeight&lt;/code&gt;, &lt;code&gt;blockNumber&lt;/code&gt;, &lt;code&gt;blockTime&lt;/code&gt;, &lt;code&gt;slotNumber&lt;/code&gt;, &lt;code&gt;currentSlot&lt;/code&gt;, &lt;code&gt;epoch&lt;/code&gt;, &lt;code&gt;currentEpoch&lt;/code&gt;, &lt;code&gt;getSlot&lt;/code&gt;, &lt;code&gt;getHeight&lt;/code&gt;, &lt;code&gt;getTimestamp&lt;/code&gt;, and about a dozen others. None of them resolve. The compiler reports "unbound identifier" for all of them.&lt;/p&gt;

&lt;p&gt;These functions may land in a future compiler version, or may require a transaction context that isn't available in the static compilation model used by the playground API. I don't know which — the compiler error doesn't distinguish between "not implemented yet" and "wrong name."&lt;/p&gt;

&lt;p&gt;If you need time-gated logic today, the workaround is to pass the block target as a circuit parameter and store it in ledger state:&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;pragma&lt;/span&gt; &lt;span class="nx"&gt;language_version&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;CompactStandardLibrary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;lockUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;setLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blockTarget&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Field&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="nx"&gt;lockUntil&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="nx"&gt;blockTarget&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 enforcement happens off-chain. The application layer reads &lt;code&gt;lockUntil&lt;/code&gt; from the ledger, checks the current block, and refuses to call the guarded circuit until the lock expires. It's not as elegant as on-chain enforcement, but it works.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Common pitfalls
&lt;/h2&gt;

&lt;p&gt;These are the issues that either break compilation silently, cause proof generation to fail with a confusing error, or create a security hole. Most of them aren't documented clearly anywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  Accessing &lt;code&gt;.value&lt;/code&gt; on &lt;code&gt;none&lt;/code&gt; fails proof generation with no message
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// If stored is none and you call this, proof generation fails.&lt;/span&gt;
&lt;span class="c1"&gt;// You don't get a clear error — the proof just can't be constructed.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;useStored&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&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;Structure your contracts so circuits that access &lt;code&gt;.value&lt;/code&gt; are only called when the value is guaranteed to be present. Separate the "set" and "use" circuits. Have the application layer check ledger state before deciding which to call.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;code&gt;CurvePoint&lt;/code&gt; and &lt;code&gt;Scalar&lt;/code&gt; are gone
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Both of these produce compiler errors in v0.30.0&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;pt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CurvePoint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// error: use JubjubPoint&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&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;Scalar&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// error: unbound identifier&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;JubjubPoint&lt;/code&gt; for the curve point type. For scalar values, use &lt;code&gt;Field&lt;/code&gt;. Both of the old names are rejected.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;code&gt;evolveNonce&lt;/code&gt; takes &lt;code&gt;Uint&amp;lt;128&amp;gt;&lt;/code&gt;, not &lt;code&gt;Bytes&amp;lt;32&amp;gt;&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// WRONG — compiler rejects Bytes&amp;lt;32&amp;gt; as the first argument&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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="nf"&gt;evolveNonce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nonce&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tag:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;// RIGHT — first argument must be Uint&amp;lt;128&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;evolveNonce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nonce&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tag:&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;The return type is &lt;code&gt;Bytes&amp;lt;32&amp;gt;&lt;/code&gt;, so a lot of articles store both the counter and the result as &lt;code&gt;Bytes&amp;lt;32&amp;gt;&lt;/code&gt;. That's wrong for the input. The counter must be &lt;code&gt;Uint&amp;lt;128&amp;gt;&lt;/code&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;code&gt;tokenType&lt;/code&gt; takes two arguments
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// WRONG — compiler rejects this (missing prefix)&lt;/span&gt;
&lt;span class="nf"&gt;tokenType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contractAddress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// RIGHT&lt;/span&gt;
&lt;span class="nf"&gt;tokenType&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;token:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;contractAddress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prefix argument is required. No overload without it exists in v0.30.0.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;code&gt;MerkleTree&amp;lt;N, T&amp;gt;&lt;/code&gt; can't live in ledger state
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// WRONG — compiler error: ADT types not supported in ledger fields&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&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;MerkleTree&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&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="c1"&gt;// RIGHT — store the accumulator root as Bytes&amp;lt;32&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;root&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="nx"&gt;persistentCommit&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&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="nx"&gt;root&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="nx"&gt;newLeaf&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;MerkleTree&lt;/code&gt; is a valid type in the type system but can't be stored as a ledger field. The error message is &lt;code&gt;algebraic data types not supported in ledger fields&lt;/code&gt;. Use &lt;code&gt;persistentCommit&lt;/code&gt; with a &lt;code&gt;Bytes&amp;lt;32&amp;gt;&lt;/code&gt; root instead.&lt;/p&gt;




&lt;h3&gt;
  
  
  Every witness-to-ledger write requires &lt;code&gt;disclose()&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// WRONG — compile error: potential witness-value disclosure&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;witness&lt;/span&gt; &lt;span class="nf"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&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;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;store&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="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;persistentHash&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&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;sk&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// RIGHT&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;store&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="nx"&gt;h&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="nx"&gt;persistentHash&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&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;sk&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 applies even when the witness value has been transformed by &lt;code&gt;persistentHash&lt;/code&gt;. The result of &lt;code&gt;persistentHash(sk())&lt;/code&gt; is still derived from a private input, so the compiler requires &lt;code&gt;disclose()&lt;/code&gt; on the assignment.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. A note on &lt;code&gt;verifyCommitment&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Some documentation and older articles reference a &lt;code&gt;verifyCommitment&lt;/code&gt; function. It's not in v0.30.0. The compiler reports it as an unbound identifier. To verify a commitment, re-derive the hash with the same inputs and compare with &lt;code&gt;assert&lt;/code&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="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;verify&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;persistentHash&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&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="nx"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;myapp:commit:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;secretValue&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="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nx"&gt;commitment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Commitment mismatch&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is equivalent and compiles cleanly.&lt;/p&gt;




&lt;h2&gt;
  
  
  12. Quick reference
&lt;/h2&gt;

&lt;p&gt;All exports verified against Compact compiler v0.30.0. The "Status" column is honest — red means I tried and it doesn't work in the current version, not that I skipped testing it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Export&lt;/th&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Signature / Type&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Maybe&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Generic&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;none&amp;lt;T&amp;gt;()&lt;/code&gt; / &lt;code&gt;some&amp;lt;T&amp;gt;(v)&lt;/code&gt; / &lt;code&gt;.value&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Either&amp;lt;L,R&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Generic&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;left&amp;lt;L,R&amp;gt;(v)&lt;/code&gt; / &lt;code&gt;right&amp;lt;L,R&amp;gt;(v)&lt;/code&gt; / &lt;code&gt;.left&lt;/code&gt; / &lt;code&gt;.right&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Counter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Type&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.increment(n)&lt;/code&gt; / &lt;code&gt;as Field&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;persistentHash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hash&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;Vector&amp;lt;N,Bytes&amp;lt;32&amp;gt;&amp;gt;&amp;gt;([...inputs])&lt;/code&gt; → &lt;code&gt;Bytes&amp;lt;32&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pad&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Utility&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;(n, str)&lt;/code&gt; → &lt;code&gt;Bytes&amp;lt;n&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;persistentCommit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Commitment&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;T&amp;gt;(root: Bytes&amp;lt;32&amp;gt;, val: Bytes&amp;lt;32&amp;gt;)&lt;/code&gt; → &lt;code&gt;Bytes&amp;lt;32&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;disclose&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Privacy&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;(value: T)&lt;/code&gt; → &lt;code&gt;T&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ZswapCoinPublicKey&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Identity&lt;/td&gt;
&lt;td&gt;Type — returned by &lt;code&gt;ownPublicKey()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ContractAddress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Identity&lt;/td&gt;
&lt;td&gt;Type — used in &lt;code&gt;tokenType()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UserAddress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Identity&lt;/td&gt;
&lt;td&gt;Type&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ownPublicKey&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Helper&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;()&lt;/code&gt; → &lt;code&gt;ZswapCoinPublicKey&lt;/code&gt; (unconstrained — see security warning)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nativeToken&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Token&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;()&lt;/code&gt; → &lt;code&gt;Bytes&amp;lt;32&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tokenType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Token&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;(prefix: Bytes&amp;lt;32&amp;gt;, addr: ContractAddress)&lt;/code&gt; → &lt;code&gt;Bytes&amp;lt;32&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;evolveNonce&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Helper&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;(nonce: Uint&amp;lt;128&amp;gt;, tag: Bytes&amp;lt;32&amp;gt;)&lt;/code&gt; → &lt;code&gt;Bytes&amp;lt;32&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shieldedBurnAddress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Helper&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;()&lt;/code&gt; → &lt;code&gt;Either&amp;lt;ZswapCoinPublicKey, ContractAddress&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;receiveShielded&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Token&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(coin: ShieldedCoinInfo)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sendShielded&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Token&lt;/td&gt;
&lt;td&gt;Requires proof server&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;JubjubPoint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Crypto&lt;/td&gt;
&lt;td&gt;Opaque type — replaces deprecated &lt;code&gt;CurvePoint&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ShieldedCoinInfo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Type&lt;/td&gt;
&lt;td&gt;Coin data for shielded transfers&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Field&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Type&lt;/td&gt;
&lt;td&gt;Native ZK field element&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Bytes&amp;lt;n&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Type&lt;/td&gt;
&lt;td&gt;Byte array of length n&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Uint&amp;lt;n&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Type&lt;/td&gt;
&lt;td&gt;Unsigned integer up to n bits&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Opaque&amp;lt;"string"&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Type&lt;/td&gt;
&lt;td&gt;Circuit-opaque string data&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MerkleTree&amp;lt;N,T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Type&lt;/td&gt;
&lt;td&gt;ADT — exists in type system, can't be stored in ledger&lt;/td&gt;
&lt;td&gt;⚠️&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getBlockTime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Block&lt;/td&gt;
&lt;td&gt;Unbound identifier in v0.30.0&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getBlockNumber&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Block&lt;/td&gt;
&lt;td&gt;Unbound identifier in v0.30.0&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getEpoch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Block&lt;/td&gt;
&lt;td&gt;Unbound identifier in v0.30.0&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CurvePoint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Crypto&lt;/td&gt;
&lt;td&gt;Deprecated — use &lt;code&gt;JubjubPoint&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;🚫&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Scalar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Crypto&lt;/td&gt;
&lt;td&gt;Unbound in v0.30.0 — use &lt;code&gt;Field&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;🚫&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;verifyCommitment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Commitment&lt;/td&gt;
&lt;td&gt;Not found in v0.30.0&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What this all adds up to
&lt;/h2&gt;

&lt;p&gt;The standard library is actually pretty small once you strip out what doesn't work yet. In practice, you're reaching for maybe eight things: &lt;code&gt;Maybe&lt;/code&gt;, &lt;code&gt;Either&lt;/code&gt;, &lt;code&gt;Counter&lt;/code&gt;, &lt;code&gt;persistentHash&lt;/code&gt;, &lt;code&gt;pad&lt;/code&gt;, &lt;code&gt;disclose&lt;/code&gt;, &lt;code&gt;ZswapCoinPublicKey&lt;/code&gt;, and whichever token functions you need. The rest is either advanced infrastructure (&lt;code&gt;receiveShielded&lt;/code&gt;, &lt;code&gt;sendShielded&lt;/code&gt;) or future capability (&lt;code&gt;getBlockTime&lt;/code&gt;, &lt;code&gt;getEpoch&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Two things tripped up every other implementation I looked at while writing this:&lt;/p&gt;

&lt;p&gt;The type renames. &lt;code&gt;CurvePoint&lt;/code&gt; and &lt;code&gt;Scalar&lt;/code&gt; still appear in community articles and the older docs. The compiler is clear about what happened — it tells you the new name in the error message — but only if you actually run your code. Articles that copy-paste from older documentation without running the compiler will keep getting this wrong.&lt;/p&gt;

&lt;p&gt;The function signatures. &lt;code&gt;evolveNonce&lt;/code&gt; takes &lt;code&gt;Uint&amp;lt;128&amp;gt;&lt;/code&gt; as its first argument, not &lt;code&gt;Bytes&amp;lt;32&amp;gt;&lt;/code&gt;. &lt;code&gt;tokenType&lt;/code&gt; takes two arguments. &lt;code&gt;persistentCommit&lt;/code&gt; takes two &lt;code&gt;Bytes&amp;lt;32&amp;gt;&lt;/code&gt; values, not a &lt;code&gt;MerkleTree&lt;/code&gt;. These aren't obscure edge cases — they're the exact calls you'd write in almost any real contract. If your code doesn't compile, it's not a reference.&lt;/p&gt;

&lt;p&gt;All seven contracts in the companion repo — &lt;a href="https://github.com/IamHarrie-Labs/compact-stdlib-reference" rel="noopener noreferrer"&gt;github.com/IamHarrie-Labs/compact-stdlib-reference&lt;/a&gt; — are standalone, one-contract-per-concept files. Run any of them through the Midnight MCP toolchain or the playground API and they'll compile cleanly.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;All code compiled and verified against Compact compiler v0.30.0 using the &lt;a href="https://www.npmjs.com/package/midnight-mcp" rel="noopener noreferrer"&gt;Midnight MCP toolchain&lt;/a&gt;. Questions or corrections? Leave a comment — if something changed in a newer compiler version, I'll update the article.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>tutorial</category>
      <category>webdev</category>
      <category>midnightfordevs</category>
    </item>
    <item>
      <title>The Zero-Knowledge Trap: Why ownPublicKey() Cannot Prove Identity in Compact</title>
      <dc:creator>Harrie</dc:creator>
      <pubDate>Thu, 23 Apr 2026 10:52:43 +0000</pubDate>
      <link>https://dev.to/iamharrie/the-zero-knowledge-trap-why-ownpublickey-cannot-prove-identity-in-compact-169i</link>
      <guid>https://dev.to/iamharrie/the-zero-knowledge-trap-why-ownpublickey-cannot-prove-identity-in-compact-169i</guid>
      <description>&lt;p&gt;For everyone who has ever written Solidity before, you should know this pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;require(msg.sender == owner, "Not the owner");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works because the EVM cryptographically verifies the transaction signature. The protocol proves the sender knows the private key, so identity verification is free.&lt;/p&gt;

&lt;p&gt;When developers arrive at Midnight and discover &lt;code&gt;ownPublicKey()&lt;/code&gt;, the instinct is similar, like with solidity: &lt;em&gt;this is my &lt;code&gt;msg.sender&lt;/code&gt;.&lt;/em&gt; It looks the same. It reads cleanly, and compiles without errors.&lt;/p&gt;

&lt;p&gt;But the problem is that ZK circuits are not the EVM. &lt;code&gt;ownPublicKey()&lt;/code&gt; does not verify what you think it verifies. In a Compact circuit, it compiles to an &lt;strong&gt;unconstrained &lt;code&gt;private_input&lt;/code&gt;&lt;/strong&gt;; a value the prover sets freely, with zero cryptographic obligation to prove they own the corresponding secret key.&lt;/p&gt;

&lt;p&gt;This article shows exactly what happens when &lt;code&gt;ownPublicKey()&lt;/code&gt; is used for access control, walks through a four-step attack against a vulnerable contract, explains why &lt;strong&gt;OpenZeppelin's &lt;code&gt;Ownable.compact&lt;/code&gt; carries this vulnerability today&lt;/strong&gt;, and demonstrates the correct fix: witness-based secret key commitment with &lt;code&gt;persistentHash&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Every Compact code block in this article has been compiled and verified against compiler &lt;strong&gt;v0.30.0&lt;/strong&gt;.&lt;/p&gt;




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

&lt;p&gt;Consider a simple private vault. A user registers as an owner and expects that only they can call privileged functions.&lt;/p&gt;

&lt;p&gt;A developer coming from Solidity would naturally write this:&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;pragma&lt;/span&gt; &lt;span class="nx"&gt;language_version&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;CompactStandardLibrary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;vault_owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ZswapCoinPublicKey&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;deposit&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="nx"&gt;vault_owner&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;ownPublicKey&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;withdraw&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="nf"&gt;ownPublicKey&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nx"&gt;vault_owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Not the vault owner&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks reasonable. The owner is recorded on &lt;code&gt;deposit&lt;/code&gt;. The &lt;code&gt;withdraw&lt;/code&gt; circuit checks the caller's public key against the stored owner. The assertion should block anyone else.&lt;/p&gt;

&lt;p&gt;It does not.&lt;/p&gt;




&lt;h2&gt;
  
  
  What &lt;code&gt;ownPublicKey()&lt;/code&gt; Actually Compiles To
&lt;/h2&gt;

&lt;p&gt;When you call a circuit on Midnight, you are not executing code on a blockchain node. You are generating a &lt;strong&gt;zero-knowledge proof&lt;/strong&gt; — a cryptographic argument that says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"I know some private inputs such that, when I run this circuit with these inputs, the computation is consistent with the current ledger state."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The ZK proof system has two categories of values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Public inputs&lt;/strong&gt; — visible to everyone: ledger state, values passed through &lt;code&gt;disclose()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private inputs (witnesses)&lt;/strong&gt; — known only to the prover: values supplied off-chain that feed into the circuit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;ownPublicKey()&lt;/code&gt; compiles to a &lt;strong&gt;private input&lt;/strong&gt;. The prover supplies it. The circuit does not constrain it to any cryptographic relationship with a secret key. It is simply a field in the proof that the prover fills in with whatever value they choose.&lt;/p&gt;

&lt;p&gt;This means the assertion:&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="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ownPublicKey&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nx"&gt;vault_owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Not the vault owner&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;does not prove the caller owns the key. It proves something far weaker:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"I know a value that equals &lt;code&gt;vault_owner&lt;/code&gt;."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And &lt;code&gt;vault_owner&lt;/code&gt; is a public ledger state — visible to everyone on the chain.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Four-Step Attack
&lt;/h2&gt;

&lt;p&gt;Here is how an attacker bypasses this access control without knowing any secret key.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Read the Owner's Public Key from the Ledger
&lt;/h3&gt;

&lt;p&gt;Ledger state in Compact is public. Any value stored with &lt;code&gt;disclose()&lt;/code&gt; is readable by anyone querying the chain.&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;// Off-chain attacker script&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ledger&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;deployedVault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ledger&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;storedOwner&lt;/span&gt; &lt;span class="o"&gt;=&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;vault_owner&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// storedOwner is now in the attacker's hands — it is just public data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Build a CircuitContext with the Spoofed Identity
&lt;/h3&gt;

&lt;p&gt;In Midnight's TypeScript SDK, the &lt;code&gt;CircuitContext&lt;/code&gt; provides the private inputs for proof generation. The attacker constructs one where &lt;code&gt;ownPublicKey()&lt;/code&gt; returns the value just read from the ledger:&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;// ownPublicKey() is unconstrained — the attacker sets it freely&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;attackContext&lt;/span&gt; &lt;span class="o"&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;defaultContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ownPublicKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;storedOwner&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;No secret key required. &lt;code&gt;ownPublicKey()&lt;/code&gt; is a free variable with no cryptographic binding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Generate a Valid ZK Proof
&lt;/h3&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;proof&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;deployedVault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prove&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withdraw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attackContext&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the circuit, the proof system evaluates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;assert(ownPublicKey() == vault_owner)
→ assert(storedOwner == storedOwner)
→ assert(true)  ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The assertion passes. The proof is valid. The attacker never needed a secret key.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Submit the Transaction
&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;await&lt;/span&gt; &lt;span class="nx"&gt;deployedVault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Transaction accepted. Privileged action executed.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The blockchain verifies the proof, finds it valid, and executes the circuit. This is not a bug in Midnight's proving system — the proof &lt;em&gt;is&lt;/em&gt; valid. It correctly proves the prover knows a value equal to &lt;code&gt;vault_owner&lt;/code&gt;. The flaw is in treating &lt;code&gt;ownPublicKey()&lt;/code&gt; as proof of key ownership when it only proves value knowledge.&lt;/p&gt;




&lt;h2&gt;
  
  
  OpenZeppelin's &lt;code&gt;Ownable.compact&lt;/code&gt;: The Ecosystem-Wide Impact
&lt;/h2&gt;

&lt;p&gt;This is not a theoretical edge case. It is present in production code that developers are actively building on.&lt;/p&gt;

&lt;p&gt;OpenZeppelin's &lt;code&gt;compact-contracts&lt;/code&gt; library, the canonical smart contract library for Midnight, directly analogous to OpenZeppelin's Solidity contracts, implements access control through &lt;code&gt;Ownable.compact&lt;/code&gt;. Here is the exact &lt;code&gt;assertOnlyOwner&lt;/code&gt; circuit from their repository:&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="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;assertOnlyOwner&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="nc"&gt;Initializable_assertInitialized&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;caller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ownPublicKey&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="nx"&gt;caller&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nx"&gt;_owner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ownable: caller is not the owner&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every contract that imports &lt;code&gt;Ownable.compact&lt;/code&gt; and calls &lt;code&gt;assertOnlyOwner()&lt;/code&gt; carries this vulnerability. The typical usage looks like:&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./compact-contracts/.../Ownable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;prefix&lt;/span&gt; &lt;span class="nx"&gt;Ownable_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;adminWithdraw&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="nc"&gt;Ownable_assertOnlyOwner&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// Does NOT prove ownership&lt;/span&gt;
  &lt;span class="c1"&gt;// ... privileged operation executes for any attacker&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The attack is identical to the vault example:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read &lt;code&gt;_owner&lt;/code&gt; from the ledger — it is stored as public state via &lt;code&gt;disclose(newOwner)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Construct a context where &lt;code&gt;ownPublicKey()&lt;/code&gt; returns &lt;code&gt;_owner.left&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Generate a valid ZK proof — the circuit evaluates &lt;code&gt;_owner.left == _owner.left&lt;/code&gt; ✓&lt;/li&gt;
&lt;li&gt;Submit the transaction — any circuit guarded by &lt;code&gt;assertOnlyOwner()&lt;/code&gt; is bypassed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The OpenZeppelin repository notes that the code is "highly experimental" and is to be used "at your own risk." But the specific mechanism of this vulnerability, that &lt;code&gt;ownPublicKey()&lt;/code&gt; is unconstrained in ZK circuits, is not surfaced prominently. Developers who see &lt;code&gt;assertOnlyOwner()&lt;/code&gt; have every reason to trust it works.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Correct Pattern: Witness + &lt;code&gt;persistentHash&lt;/code&gt; Commitment
&lt;/h2&gt;

&lt;p&gt;The fix requires a shift in its concept.&lt;/p&gt;

&lt;p&gt;Instead of asking &lt;em&gt;"what is the caller's public key?"&lt;/em&gt;, you ask &lt;em&gt;"Can the caller prove they know a secret whose hash matches the value stored on-chain?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is a commitment scheme. The owner registration stores a &lt;strong&gt;cryptographic hash&lt;/strong&gt; of a secret key. Ownership verification requires the caller to re-derive that same hash, which is only possible if they know the original secret.&lt;/p&gt;

&lt;p&gt;Midnight's own &lt;code&gt;example-bboard&lt;/code&gt; contract uses this pattern correctly. Here is the relevant portion of the actual &lt;code&gt;bboard.compact&lt;/code&gt; source:&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;witness&lt;/span&gt; &lt;span class="nf"&gt;localSecretKey&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&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;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Opaque&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&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="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="nx"&gt;state&lt;/span&gt; &lt;span class="o"&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;VACANT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Attempted to post to an occupied board&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;owner&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;publicKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;localSecretKey&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;sequence&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Field&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;message&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="nx"&gt;some&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Opaque&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;newMessage&lt;/span&gt;&lt;span class="p"&gt;));&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;State&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OCCUPIED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;takeDown&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;Opaque&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&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="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&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;State&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OCCUPIED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Attempted to take down post from an empty board&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;owner&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;localSecretKey&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;sequence&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Field&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Attempted to take down post, but not the current owner&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;formerMsg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&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;State&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VACANT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;increment&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="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;none&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Opaque&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;formerMsg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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="nx"&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="nx"&gt;persistentHash&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&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="nx"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bboard:pk:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sk&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 things make this secure:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;witness localSecretKey(): Bytes&amp;lt;32&amp;gt;&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
The secret key is a private input, but the circuit &lt;em&gt;constrains&lt;/em&gt; it through the hash computation. It is not a free variable — its value must produce a hash that matches the stored commitment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;persistentHash&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
The owner is stored as a hash of &lt;code&gt;[domain_prefix, sequence, secret_key]&lt;/code&gt;. This is a one-way commitment. Knowing the output tells you nothing about the input.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The ownership check&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;takeDown()&lt;/code&gt; re-derives the same hash and asserts equality. The ZK proof now proves: &lt;em&gt;"I know an &lt;code&gt;sk&lt;/code&gt; such that &lt;code&gt;hash(prefix, sequence, sk) == owner&lt;/code&gt;."&lt;/em&gt; An attacker who does not know &lt;code&gt;sk&lt;/code&gt; cannot produce a valid proof, because &lt;code&gt;persistentHash&lt;/code&gt; is one-way.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Secure Vault: Fixed Implementation
&lt;/h2&gt;

&lt;p&gt;Applying the commitment pattern to the vulnerable vault. This compiles cleanly against v0.30.0:&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;pragma&lt;/span&gt; &lt;span class="nx"&gt;language_version&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;CompactStandardLibrary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;ledger&lt;/span&gt; &lt;span class="nx"&gt;vault_owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;witness&lt;/span&gt; &lt;span class="nf"&gt;localSecretKey&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;ownerCommitment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&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="nx"&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="nx"&gt;persistentHash&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&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="nx"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vault:owner:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;deposit&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="nx"&gt;vault_owner&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;ownerCommitment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;localSecretKey&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;withdraw&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="nf"&gt;ownerCommitment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;localSecretKey&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nx"&gt;vault_owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Not the vault owner&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The off-chain witnesses file provides the secret key from private state:&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;// witnesses.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;WitnessContext&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-ntwrk/compact-runtime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;VaultPrivateState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&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="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createVaultPrivateState&lt;/span&gt; &lt;span class="o"&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;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;VaultPrivateState&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;secretKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&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="na"&gt;localSecretKey&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="nx"&gt;privateState&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;VaultPrivateState&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;VaultPrivateState&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="o"&gt;=&amp;gt;&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="nx"&gt;privateState&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now replay the attack. The attacker reads &lt;code&gt;vault_owner&lt;/code&gt; from the ledger — they get a 32-byte hash value. They cannot reverse &lt;code&gt;persistentHash&lt;/code&gt; to find &lt;code&gt;sk&lt;/code&gt;. When they attempt to generate a proof for &lt;code&gt;withdraw()&lt;/code&gt;, the circuit requires them to supply an &lt;code&gt;sk&lt;/code&gt; such that &lt;code&gt;hash("vault:owner:", sk) == vault_owner&lt;/code&gt;. Without the original secret key, no valid proof can be constructed. The attack fails.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the Domain Prefix Matters
&lt;/h2&gt;

&lt;p&gt;Notice &lt;code&gt;pad(32, "vault:owner:")&lt;/code&gt; in the &lt;code&gt;persistentHash&lt;/code&gt; call. This is &lt;strong&gt;domain separation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you use the same hash function and inputs across multiple commitments in a contract, an attacker might satisfy one circuit's constraint by reusing a proof from a different circuit — a cross-context replay attack.&lt;/p&gt;

&lt;p&gt;The domain prefix ensures that a commitment built for vault ownership cannot satisfy constraints elsewhere in the contract. The &lt;code&gt;example-bboard&lt;/code&gt; uses &lt;code&gt;"bboard:pk:"&lt;/code&gt; for exactly this reason.&lt;/p&gt;

&lt;p&gt;Establish a naming convention for your own contracts: &lt;code&gt;"contractname:purpose:"&lt;/code&gt; — and never reuse the same prefix across different commitment types.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing Your Implementation
&lt;/h2&gt;

&lt;p&gt;Compiling without errors is not security. A test suite for this pattern must verify three scenarios.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test 1: The Legitimate Owner Can Call Withdraw
&lt;/h3&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;ownerSecretKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&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;Uint8Array&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ownerState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createVaultPrivateState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ownerSecretKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callCircuit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deposit&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;ownerState&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Should succeed&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="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callCircuit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;withdraw&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;ownerState&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeDefined&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test 2: A Different Key Holder Cannot Call Withdraw
&lt;/h3&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;attackerSecretKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&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;Uint8Array&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;attackerState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createVaultPrivateState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attackerSecretKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Should throw — attacker's commitment does not match stored owner&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;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callCircuit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;withdraw&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;attackerState&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test 3: An Attacker Using the Stored Ledger Value Directly Cannot Withdraw
&lt;/h3&gt;

&lt;p&gt;This test directly reproduces the &lt;code&gt;ownPublicKey()&lt;/code&gt; attack pattern against the fixed contract:&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;// Attacker reads the public ledger value&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ledger&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;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ledger&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;storedOwner&lt;/span&gt; &lt;span class="o"&gt;=&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;vault_owner&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Attacker attempts to use the stored hash as if it were the secret key&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fakeState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createVaultPrivateState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;storedOwner&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Should fail — persistentHash("vault:owner:", stored_hash) != persistentHash("vault:owner:", original_sk)&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;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callCircuit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;withdraw&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;fakeState&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If all three pass, you have verified correctness and resistance to the &lt;code&gt;ownPublicKey()&lt;/code&gt; class of attack.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Mental Model
&lt;/h2&gt;

&lt;p&gt;When reviewing any Compact access control circuit, apply one question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Does this proof require the caller to demonstrate knowledge of a secret, or only knowledge of a public value?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;What it proves&lt;/th&gt;
&lt;th&gt;Secure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;assert(ownPublicKey() == ledger.owner)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Caller knows &lt;code&gt;ledger.owner&lt;/code&gt; (public)&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;assert(persistentHash([prefix, witness()]) == ledger.owner)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Caller knows the preimage of &lt;code&gt;ledger.owner&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rule: &lt;strong&gt;public values cannot gate access.&lt;/strong&gt; If the value being compared against is visible on-chain, any prover can satisfy the assertion without owning any secret. Access control must be gated on something the prover must know but cannot observe — a preimage, a secret key.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ownPublicKey()&lt;/code&gt; feels like it should be that secret. In the EVM, the transaction signature is the proof. In a ZK circuit, &lt;code&gt;ownPublicKey()&lt;/code&gt; is just a number the prover fills in. The circuit has no mechanism to bind it to an externally held secret.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;ownPublicKey()&lt;/code&gt; vulnerability is not a bug in Midnight. It is a consequence of the fundamental difference between EVM identity and ZK identity.&lt;/p&gt;

&lt;p&gt;In the EVM, the protocol enforces that &lt;code&gt;msg.sender&lt;/code&gt; matches a transaction signature. In a ZK circuit, &lt;strong&gt;the circuit itself must enforce identity&lt;/strong&gt; — by requiring the prover to demonstrate knowledge of a secret whose commitment is stored on-chain.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ownPublicKey()&lt;/code&gt; skips that enforcement. It gives developers a familiar-looking API that silently removes the security guarantee they are relying on.&lt;/p&gt;

&lt;p&gt;The fix: declare a &lt;code&gt;witness localSecretKey(): Bytes&amp;lt;32&amp;gt;&lt;/code&gt;, store a &lt;code&gt;persistentHash&lt;/code&gt; commitment, and verify by re-deriving the commitment. Every access control pattern in Compact should follow this structure — the same structure the Midnight Foundation used in &lt;code&gt;example-bboard&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;OpenZeppelin's &lt;code&gt;Ownable.compact&lt;/code&gt; uses &lt;code&gt;ownPublicKey()&lt;/code&gt; in &lt;code&gt;assertOnlyOwner()&lt;/code&gt;. Every contract using that library today is vulnerable until it is patched.&lt;/p&gt;

&lt;p&gt;Before shipping any contract with ownership or role-based access control, apply the mental model test: &lt;em&gt;Does proving ownership require the caller to know something that is not already visible on-chain?&lt;/em&gt; If the answer is no, the access control does not work.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;All Compact code examples compiled and verified against Compact compiler v0.30.0 using the Midnight MCP toolchain. Reference implementation: &lt;a href="https://github.com/midnightntwrk/example-bboard" rel="noopener noreferrer"&gt;midnightntwrk/example-bboard&lt;/a&gt;. Questions or corrections? Drop a comment below.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>security</category>
      <category>webdev</category>
      <category>midnightfordevs</category>
    </item>
  </channel>
</rss>
