<?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: Lucas Oliveira</title>
    <description>The latest articles on DEV Community by Lucas Oliveira (@olivmath).</description>
    <link>https://dev.to/olivmath</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%2F3813873%2F2a0335ad-8c08-4dde-91d8-c05ae6839487.png</url>
      <title>DEV Community: Lucas Oliveira</title>
      <link>https://dev.to/olivmath</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/olivmath"/>
    <language>en</language>
    <item>
      <title>DevLog: ZK Battleship - part 2</title>
      <dc:creator>Lucas Oliveira</dc:creator>
      <pubDate>Tue, 10 Mar 2026 16:32:50 +0000</pubDate>
      <link>https://dev.to/olivmath/devlog-zk-battleship-part-2-25lp</link>
      <guid>https://dev.to/olivmath/devlog-zk-battleship-part-2-25lp</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&amp;gt;&amp;gt;&amp;gt; ARTICLE HANDWRITTEN WITH GRAMMAR CORRECTIONS BY A.I. &amp;lt;&amp;lt;&amp;lt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  February 21st: Actually Learning ZK
&lt;/h2&gt;

&lt;p&gt;The single-player game was ready. Beautiful, playable, with 14 screens and animations. But so far there wasn't a single line of ZK code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But how do you write a zero-knowledge proof?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You don't write the Zero-Knowledge Proof (ZKP) directly. You write a circuit. A circuit is a program that describes the rules the proof must satisfy. Then, a mathematical tool uses the circuit along with the input data, both public and private, to generate the Zero-Knowledge Proof — which we'll simply call a proof.&lt;/p&gt;

&lt;p&gt;These rules are called &lt;strong&gt;constraints&lt;/strong&gt; and will generate a proof of computation. A proof that the code executed correctly without revealing the execution's input data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Noir: The Language of Circuits
&lt;/h2&gt;

&lt;p&gt;There are several languages for writing ZK circuits: Circom, Cairo, Noir, among others. I chose Noir for three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The syntax is similar to Rust: so I already "knew" how to program.&lt;/li&gt;
&lt;li&gt;Toolchain: very practical to install, compile, test, and generate proofs.&lt;/li&gt;
&lt;li&gt;Compatible proof system: Stellar's Protocol 25 is fully compatible with the UltraHonk proof system. (both use the same BN254 elliptic curve and Poseidon2 hash function)&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Board Circuit
&lt;/h2&gt;

&lt;p&gt;The most important circuit is the first one: proving that your board is valid without revealing where the ships are.&lt;/p&gt;

&lt;p&gt;What it needs to verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prove that the board being used is the one being proven (public commitment)&lt;/li&gt;
&lt;li&gt;The ships have the correct sizes (5, 4, 3, 3, 2)&lt;/li&gt;
&lt;li&gt;No ship goes out of the 10x10 board bounds&lt;/li&gt;
&lt;li&gt;No ship overlaps another&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I modeled the board as a 10x10 matrix and the ships as vectors of type &lt;code&gt;A[i][j]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I modeled the constraints as if/else functions that should break the entire circuit if any failed. This way, either everything is valid or nothing is, making the circuit &lt;strong&gt;atomic&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And to turn these constraints into code, I used Noir like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;poseidon&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;poseidon2&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Poseidon2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ships&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;      &lt;span class="c1"&gt;// Ship coordinates (SECRET)&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;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                        &lt;span class="c1"&gt;// random number (SECRET)&lt;/span&gt;
    &lt;span class="n"&gt;board_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;               &lt;span class="c1"&gt;// public commitment&lt;/span&gt;
    &lt;span class="n"&gt;ship_sizes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;             &lt;span class="c1"&gt;// expected sizes&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. Validate the public commitment&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;computed_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compute_board_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ships&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;computed_hash&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;board_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"invalid board hash"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Validate ship sizes&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;in&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;5&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ships&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ship_sizes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s"&gt;"invalid ship size"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Validate board bounds&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;in&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;5&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;horizontal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ships&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ship_in_bounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;horizontal&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 4. Validate ship overlap&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;in&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;5&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;ships_overlap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ships&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ships&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
        &lt;span class="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;Notice the &lt;code&gt;pub&lt;/code&gt; keyword. Parameters marked with &lt;code&gt;pub&lt;/code&gt; are public and anyone can see them. Parameters without &lt;code&gt;pub&lt;/code&gt; are private — only the proof generator knows them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ships&lt;/code&gt;: coordinates of your ships.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nonce&lt;/code&gt;: random number to prevent someone from testing all possible board combinations until they find the &lt;code&gt;board_hash&lt;/code&gt; and discover the actual positions.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;board_hash&lt;/code&gt; and &lt;code&gt;ship_sizes&lt;/code&gt;: everyone can verify that the proof was generated against that specific hash and with those ship sizes.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  SHA256 vs Poseidon2
&lt;/h2&gt;

&lt;p&gt;A detail that seems small but is one of the most important decisions of the project: which hash function to use.&lt;/p&gt;

&lt;p&gt;In the normal world, you'd use SHA-256 for its simplicity and because any programming language has it available natively.&lt;/p&gt;

&lt;p&gt;I even started with SHA-256 to speed up the MVP, but inside a ZK circuit, SHA-256 is extremely expensive and slow. Each bitwise operation becomes thousands of constraints after the circuit is compiled, making the proof slow to generate and expensive to verify.&lt;/p&gt;

&lt;p&gt;Poseidon2 is a hash function designed specifically for ZK circuits. It operates over finite fields instead of bits, making it orders of magnitude more efficient inside a circuit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;compute_board_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ships&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="mi"&gt;5&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;Field&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;21&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;in&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;5&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ships&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&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="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// row&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;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&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="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// column&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;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&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="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// size&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;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;h&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;else&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="c1"&gt;// orientation&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                             &lt;span class="c1"&gt;// random salt&lt;/span&gt;
    &lt;span class="nn"&gt;Poseidon2&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;21&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;5 ships × 4 fields each = 20 inputs + 1 nonce = 21 bytes total. Poseidon2 transforms all inputs into a single Field element of 254 bits. This is the public commitment of the board.&lt;/p&gt;

&lt;p&gt;The nonce is crucial: without it, someone could try all possible board combinations and compare against the public hash. The nonce makes this impossible — it's a random salt that only you know.&lt;/p&gt;

&lt;p&gt;And here's the bonus: Stellar implemented Poseidon2 as a native instruction in Protocol 25. This means that when we verify the hash on-chain, we're not executing smart contract code — we're actually using a native protocol operation. Fast and cheap.&lt;/p&gt;




&lt;h2&gt;
  
  
  Attack Circuit
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;With the board validated, the next problem is ensuring honesty during the match.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Beyond proving the board is valid, we also need to prove that each action during the game is honest.&lt;/p&gt;

&lt;p&gt;When my opponent attacks coordinate (3, 5) on my board, I say "hit" or "miss". But how do they know I'm not lying?&lt;/p&gt;

&lt;p&gt;I wrote 2 constraints for this proof:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The hash I'm using must match the board from the beginning.&lt;/li&gt;
&lt;li&gt;If there's a ship at that coordinate, I say &lt;code&gt;true&lt;/code&gt;; if not, &lt;code&gt;false&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If I try to lie (say "miss" when they actually "hit" a ship), the circuit fails and the proof cannot be generated. Lying without breaking the proof system's security is mathematically infeasible.&lt;/p&gt;

&lt;p&gt;And the code for this looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ships&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;    &lt;span class="c1"&gt;// my positions (SECRET)&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;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                      &lt;span class="c1"&gt;// my nonce (SECRET)&lt;/span&gt;
    &lt;span class="n"&gt;board_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;// public commitment&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                       &lt;span class="c1"&gt;// attacked X coordinate&lt;/span&gt;
    &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                       &lt;span class="c1"&gt;// attacked Y coordinate&lt;/span&gt;
    &lt;span class="n"&gt;is_hit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                  &lt;span class="c1"&gt;// my response&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Does the hash match the board I committed at the start?&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;computed_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compute_board_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ships&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;computed_hash&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;board_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"invalid board hash"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Is the response consistent with the actual ship positions?&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;actual_hit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cell_is_hit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ships&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual_hit&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;is_hit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"dishonest result"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;But wait — why is &lt;code&gt;is_hit&lt;/code&gt; an input and not a return value from the circuit?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ZK circuits don't return values. They are constraint systems. This means they only do &lt;code&gt;assert&lt;/code&gt; — they pass or fail. If the circuit could return the calculated result of &lt;code&gt;cell_is_hit&lt;/code&gt;, it would have to expose something derived from &lt;code&gt;ships&lt;/code&gt;, which is private. This would leak information about where the ships are.&lt;/p&gt;

&lt;p&gt;In the world of circuits, the pattern is the &lt;strong&gt;inverse&lt;/strong&gt; of regular programming:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You declare the result (&lt;code&gt;is_hit = true&lt;/code&gt;), and the circuit proves that your declaration is true given the secret data. If your declaration is false, the proof simply cannot be generated because the circuit fails.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Final Match Circuit
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;With board validation and individual attack verification solved, what remained was the referee for the entire match.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The final circuit is the most ambitious: replaying the entire match and calculating, step by step, the winner without revealing the boards, in a mathematically verifiable way.&lt;/p&gt;

&lt;p&gt;This circuit is the final referee. It receives the entire match history — all attacks from both players — and recalculates who won. If someone tries to declare the wrong winner, the proof fails.&lt;/p&gt;

&lt;p&gt;ZK circuits in Noir require arrays to have fixed sizes at compile time. That's why I used an attack array with a fixed size of 100 (which is the maximum for a 10x10 board). Unused slots are filled with &lt;code&gt;(0, 0)&lt;/code&gt; and ignored via the &lt;code&gt;n_attacks&lt;/code&gt; counter.&lt;/p&gt;

&lt;p&gt;This circuit is the largest, slowest, and most expensive of the three. Even so, the on-chain verification cost on Stellar was only &lt;strong&gt;$0.00032 USD&lt;/strong&gt;. This is because it has to run 2 loops to count who hit all of the opponent's ships:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="c1"&gt;// Player 1&lt;/span&gt;
    &lt;span class="n"&gt;ships_player&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;// player 1's ships (SECRET)&lt;/span&gt;
    &lt;span class="n"&gt;attacks_player&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;    &lt;span class="c1"&gt;// all player 1's attacks&lt;/span&gt;
    &lt;span class="n"&gt;board_hash_player&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;n_attacks_player&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;nonce_player&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;// Player 2 (AI)&lt;/span&gt;
    &lt;span class="n"&gt;ships_ai&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;      &lt;span class="c1"&gt;// player 2's ships (SECRET)&lt;/span&gt;
    &lt;span class="n"&gt;attacks_ai&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;        &lt;span class="c1"&gt;// all player 2's attacks&lt;/span&gt;
    &lt;span class="n"&gt;board_hash_ai&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;n_attacks_ai&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;nonce_ai&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;// Match&lt;/span&gt;
    &lt;span class="n"&gt;ship_sizes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;winner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                         &lt;span class="c1"&gt;// player 1 or player 2&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Verify both boards&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;compute_board_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ships_player&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nonce_player&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;board_hash_player&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;compute_board_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ships_ai&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nonce_ai&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;board_hash_ai&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Count player 1's hits&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;hits_on_player&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;in&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;100&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;n_attacks_ai&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attacks_ai&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;cell_is_hit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ships_player&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;hits_on_player&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="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;// Count player 2's (AI) hits&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;hits_on_ai&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;in&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;100&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;n_attacks_player&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attacks_player&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;cell_is_hit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ships_ai&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;hits_on_ai&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="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;// 5+4+3+3+2 = 17 total ship cells&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;total_ship_counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Did the declared winner actually win?&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;winner&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="c1"&gt;// Did P1 sink all of P2's ships?&lt;/span&gt;
        &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hits_on_ai&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;total_ship_counter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Did P2 sink all of P1's ships?&lt;/span&gt;
        &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hits_on_player&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;total_ship_counter&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;
  
  
  Integrating the Circuits
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;With all three circuits ready, the next step was understanding where and how they run.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now that the circuit is ready, it needs to receive data to start generating proofs. But where does this language run? Node.js? Rust? In the browser?&lt;/p&gt;

&lt;p&gt;I'll compare the circuit lifecycle to smart contracts and, if you're from web2, to functions-as-a-service:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Web2 - Functions-as-a-Service:&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;Write → Compile → Test → Deploy to Cloud → Interact (cold start)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Web3 - Smart contracts:&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;Write → Compile → Test → Deploy to Blockchain (EVM/Soroban) → Interact (gas fee)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Web3 ZK - Circuits:&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;Write → Compile → Test → Import in client → Interact (CPU) → Output (proof)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unlike Web2 and Web3, circuits &lt;strong&gt;should not be executed on the server side&lt;/strong&gt;. Think about it: if you have to send your data to the server to generate a proof with secret inputs, you've already revealed your data to the network and to the server.&lt;/p&gt;

&lt;p&gt;That's why circuits should be compiled and injected into the client, just like a WASM library or a JS asset that runs on the client.&lt;/p&gt;

&lt;p&gt;After writing the circuits, compilation generates an executable binary capable of dynamically generating proofs with secret inputs. But compilation also generates another important artifact: the &lt;strong&gt;Verification Key (VK)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The VK is the public half of the circuit — it contains everything a verifier needs to confirm that a proof is legitimate, without needing the full circuit or the secret data. In my project, the VKs for the board and match circuits are embedded inside the Soroban smart contract, which uses them to verify proofs on-chain. The attack circuit's VK stays only on the backend since it's verified off-chain.&lt;/p&gt;

&lt;p&gt;To generate a proof from a Noir circuit, you need two components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NoirJS&lt;/strong&gt; (&lt;code&gt;@noir-lang/noir_js&lt;/code&gt;): executes the circuit and generates the &lt;strong&gt;witness&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;bb.js&lt;/strong&gt; (&lt;code&gt;@aztec/bb.js&lt;/code&gt;): takes the witness and generates the proof&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is the witness?&lt;/strong&gt; It's the set of all intermediate values of the circuit during execution — like a complete trace of every wire in the circuit. The proof is generated from this witness.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On Node.js, it works like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Import the libs&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;Noir&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;@noir-lang/noir_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;UltraHonkBackend&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;@aztec/bb.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Import the circuit as an executable binary&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;binary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;board_validity.json&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;utf-8&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;circuit&lt;/span&gt; &lt;span class="o"&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;binary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Instantiate the circuit&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;backend&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;UltraHonkBackend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytecode&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;noir&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;Noir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Generate witness (execute the circuit with secret inputs)&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;witness&lt;/span&gt; &lt;span class="p"&gt;}&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;noir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;ships&lt;/span&gt;&lt;span class="p"&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;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&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="mi"&gt;2&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;4&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="na"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;12345&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;board_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0x07488bfc...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ship_sizes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&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="mi"&gt;3&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="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="c1"&gt;// Generate the proof: a Uint8Array artifact of 14592 bytes&lt;/span&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;backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateProof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;witness&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;keccak&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;{ keccak: true }&lt;/code&gt; flag is unrelated to Poseidon2. Poseidon2 is the hash &lt;strong&gt;we&lt;/strong&gt; use to hash the board — it's part of the game logic. This flag controls a different hash, used by &lt;strong&gt;bb.js itself&lt;/strong&gt; under the hood when assembling the proof. By default it uses Blake3, but smart contracts don't have Blake3 — they have Keccak256. The flag switches to Keccak256, allowing the on-chain contract to verify the proof.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mobile ZK
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Node.js worked. The next challenge: running this on a mobile app.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On the backend (Node.js), this worked on the first try. But the plan was to generate proofs on the client, which in my case would be the mobile app. The player positions the ships, the app generates the proof locally, and sends only the proof to the backend (never the positions).&lt;/p&gt;

&lt;p&gt;And here the fun began.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bb.js&lt;/code&gt; uses heavy WebAssembly. It loads megabytes of binaries to perform cryptographic operations. On Node.js and the browser, this works because both have mature WASM support. But React Native...&lt;/p&gt;

&lt;p&gt;React Native doesn't run in a browser. It runs on its own JavaScript runtime (Hermes or JSC) that doesn't have full WebAssembly support. &lt;code&gt;bb.js&lt;/code&gt; needs WASM threads, shared memory, and APIs that simply don't exist in the React Native runtime.&lt;/p&gt;

&lt;p&gt;I tried several approaches:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 1: import bb.js directly in React Native. (HACK)&lt;/strong&gt;&lt;br&gt;
Failed immediately. WebAssembly doesn't exist in Hermes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 2: invisible WebView. (MASTER HACK)&lt;/strong&gt;&lt;br&gt;
The idea: create a transparent &lt;code&gt;&amp;lt;WebView&amp;gt;&lt;/code&gt; that loads an HTML page with bb.js, and communicate via &lt;code&gt;postMessage&lt;/code&gt;. The WebView runs on a real browser engine, so WASM should work.&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="c1"&gt;// zk/adapter.tsx — WebView provider&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;WebView&lt;/span&gt;
    &lt;span class="na"&gt;source&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;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;zkWorkerHtml&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;onMessage&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="nx"&gt;event&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;result&lt;/span&gt; &lt;span class="o"&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nativeEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// Proof generated!&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This almost worked. I deluded myself thinking the proofs "were just slow", that "I'd refine it later". I waited 20 minutes for proof generation and nothing — just burning memory and CPU. And the communication via &lt;code&gt;postMessage&lt;/code&gt; added extra latency on top of that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 3: version downgrade.&lt;/strong&gt;&lt;br&gt;
I thought maybe an older version of bb.js would be lighter. Tested bb.js 0.72.1 with noir_js beta.2. Still didn't run on React Native.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Painful Decision
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;With three failed attempts, it was time to accept the cost of the previous decision.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I looked at the mobile app. 14 screens. Animations + haptic feedback as reactions. Animated splash screen with radar, 3D ship model, confetti...&lt;/p&gt;

&lt;p&gt;In short, I had Claude Code migrate everything to a web client, reusing everything it could. I used basic Vite + React to make everything work in the browser.&lt;/p&gt;

&lt;p&gt;The browser is a stable environment today and you can execute many things natively like WASM and other cryptography APIs. Everything worked and I didn't lose much of the app's beauty.&lt;/p&gt;

&lt;p&gt;The technical learning was about how to handle imports in the browser to correctly load the circuit and libraries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. Import compiled circuits as static JSON&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;boardValidityCircuit&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;./circuits/board_validity.json&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="nx"&gt;shotProofCircuit&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;./circuits/shot_proof.json&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="nx"&gt;turnsProofCircuit&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;./circuits/turns_proof.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Import libs dynamically (heavy WASM, can't be bundled)&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;noirMod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bbMod&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@noir-lang/noir_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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@aztec/bb.js&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;// 3. Instantiate the circuit with NoirJS&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;noir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;noirMod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Noir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;boardValidityCircuit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 4. Execute the circuit and generate the proof&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;witness&lt;/span&gt; &lt;span class="p"&gt;}&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;noir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputs&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;backend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;bbMod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UltraHonkBackend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;boardValidityCircuit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytecode&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;proof&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;publicInputs&lt;/span&gt; &lt;span class="p"&gt;}&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;backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateProof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;witness&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;keccak&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compiled circuits (&lt;code&gt;.json&lt;/code&gt;) are imported as static assets — Vite resolves this at build time. NoirJS and bb.js are imported dynamically because they load heavy WASM that can't be pre-bundled (in &lt;code&gt;vite.config.ts&lt;/code&gt; they go in &lt;code&gt;optimizeDeps.exclude&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Here's another learning, this time non-technical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I should have written a hello world circuit and validated that it was possible to generate the proof on mobile on day 0, before starting the polish.&lt;/strong&gt; But I fell in love.&lt;/p&gt;

&lt;p&gt;I fell in love because it was one of the coolest programming experiences I've ever had. In a game, it's super fun to test things because you're playing while you program — very different from testing signup screens or seeing if Docker will compile correctly this time.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We Have So Far
&lt;/h2&gt;

&lt;p&gt;At the end of this phase:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;3 Noir circuits compiled and tested (&lt;code&gt;board_validity&lt;/code&gt;, &lt;code&gt;shot_proof&lt;/code&gt;, &lt;code&gt;turns_proof&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;100% compatible with Stellar Protocol 25 (Poseidon2 + BN254)&lt;/li&gt;
&lt;li&gt;Circuits generating proofs on the web client (browser + bb.js + NoirJS)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The circuits were ready. The proofs were being generated. But nobody was verifying them yet — neither the backend nor the blockchain.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Next: Part 3 — "The blockchain that verified the proof"&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devjournal</category>
      <category>gamedev</category>
      <category>learning</category>
      <category>privacy</category>
    </item>
    <item>
      <title>DevLog: ZK Battleship - part 1</title>
      <dc:creator>Lucas Oliveira</dc:creator>
      <pubDate>Mon, 09 Mar 2026 05:30:23 +0000</pubDate>
      <link>https://dev.to/olivmath/logbook-zk-battleship-part-1-2fi5</link>
      <guid>https://dev.to/olivmath/logbook-zk-battleship-part-1-2fi5</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&amp;gt;&amp;gt;&amp;gt; ARTICLE HANDWRITTEN WITH GRAMMAR CORRECTIONS BY A.I. &amp;lt;&amp;lt;&amp;lt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  The Hackathon
&lt;/h1&gt;

&lt;p&gt;February 2026. The Stellar Development Foundation announces &lt;strong&gt;Stellar Hacks: ZK Gaming&lt;/strong&gt; — a hackathon to build on-chain games using zero-knowledge proofs. The prize? Recognition, feedback from Stellar devs, and the chance to show that ZK isn't just academic paper stuff.&lt;/p&gt;

&lt;p&gt;I signed up without ever having made a game before. I thought it would be just another project with smart contracts and maybe a bit more interesting because it required implementing zero-knowledge proofs. In my head, writing a Battleship would be perfect to integrate with ZK.&lt;/p&gt;

&lt;p&gt;This is the devlog of how I built &lt;strong&gt;Stealth Battleship&lt;/strong&gt; in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;17 days&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;331 commits&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;4 languages&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Problem: Someone always sees both boards.
&lt;/h2&gt;

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

&lt;p&gt;You know Battleship. Two players, each with a board. You place your ships, the opponent places theirs. Nobody sees the other's board. You attack a coordinate, the opponent says "hit" or "miss".&lt;/p&gt;

&lt;p&gt;Simple in real life, where each player has their board behind a barrier. But in the digital world, who guards the boards?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Approach 1: the server sees everything.&lt;/strong&gt; The server knows where both players' ships are. It works, but you need to trust that the server won't cheat. If the server wants to favor a player, it can, and you'd never know.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Approach 2: commit-reveal.&lt;/strong&gt; Each player hashes their board at the start and publishes the hash. At the end, they reveal the original board and everyone checks if it matches the hash. Seems elegant, but it has a vulnerability: if the player is losing, they simply disconnect. Never reveals. Nobody can prove anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Approach 3: board on-chain.&lt;/strong&gt; Put everything on the blockchain! Verifiable and immutable. Except that... the blockchain is public. Anyone can read the blocks and see the transactions before they're confirmed. Your opponent would see your ships before the game even starts.&lt;/p&gt;

&lt;p&gt;None of the three works. They all fail for a fundamental reason: &lt;strong&gt;at some point, someone sees information they shouldn't see&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Solution: What if nobody needed to see?
&lt;/h2&gt;

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

&lt;p&gt;Zero-Knowledge Proofs (ZK) solve exactly this. The idea is simple to understand, but profound in its consequences:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I can &lt;strong&gt;prove&lt;/strong&gt; to you that something is true, &lt;strong&gt;without showing you&lt;/strong&gt; the information that makes it true.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Simple.&lt;/p&gt;

&lt;p&gt;Imagine I'm preparing a surprise marriage proposal. I asked you, a craftsman, to create a diamond ring. But you can't show the ring to anyone, not even to me, because that would ruin the surprise.&lt;/p&gt;

&lt;p&gt;Even so, you make a claim:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The diamond on the ring is real.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But I want to be sure that's true. Except seeing the ring would ruin everything.&lt;/p&gt;

&lt;p&gt;So I take the ring to a diamond specialist. He examines the stone and, after running the necessary tests, hands me a certificate saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The analyzed diamond is real.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;The stone passed all authenticity tests.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I never saw:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The ring.&lt;/li&gt;
&lt;li&gt;The tests that could reveal characteristics about it.&lt;/li&gt;
&lt;li&gt;The size, shape, color, value, or any other detail about the ring.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Still, I have a proof. I have zero knowledge about the ring, except for the fact that it is real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I have a Proof and Zero Knowledge about the fact.&lt;/strong&gt;&lt;br&gt;
If you lie, the specialist will find out. Cheating is impossible.&lt;/p&gt;




&lt;h2&gt;
  
  
  Applying ZK to Battleship
&lt;/h2&gt;

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

&lt;p&gt;The diamond will be the board and the specialist, the math. With this idea, the game works like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start&lt;/strong&gt;: each player places their ships and generates a ZK Proof that the board is valid. The proof is public; the board is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each move&lt;/strong&gt;: when I attack coordinate (3, 5) on my opponent's board, they tell me "hit" or "miss" — and along with it send a ZK Proof that the answer is honest against the original board.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;End&lt;/strong&gt;: when all ships are sunk, the entire match is replayed inside a circuit that deterministically calculates the winner and generates a final ZK Proof.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nobody ever sees the other's board. There's no omniscient server. There's no reveal at the end that can be avoided. Math guarantees everything.&lt;/p&gt;




&lt;h2&gt;
  
  
  But does ZK work on Stellar?
&lt;/h2&gt;

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

&lt;p&gt;Stellar just launched &lt;strong&gt;Protocol 25&lt;/strong&gt;, codename &lt;strong&gt;X-Ray&lt;/strong&gt;. This upgrade added advanced cryptography operations — &lt;strong&gt;BN254&lt;/strong&gt;, &lt;strong&gt;Poseidon2&lt;/strong&gt;, and &lt;strong&gt;BLS12-381&lt;/strong&gt; — directly into the protocol. Not in smart contracts. Not on L2. In the protocol itself.&lt;/p&gt;

&lt;p&gt;To understand the impact: verifying a ZK proof requires heavy math operations — elliptic curve point multiplication, hashing, pairings. On other blockchains, this runs inside smart contracts, which is slow and expensive. On Stellar, these operations are now native protocol instructions.&lt;/p&gt;

&lt;p&gt;The result? Verifying a ZK Proof on Stellar costs around &lt;strong&gt;$0.005 USD&lt;/strong&gt;. Orders of magnitude cheaper than on any other blockchain.&lt;/p&gt;

&lt;p&gt;(Yes, I thought it was $30 USD. I explain this mistake in part 3).&lt;/p&gt;




&lt;h2&gt;
  
  
  The First Commit: February 7, 2026.
&lt;/h2&gt;

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

&lt;p&gt;After 48 hours, I already had a playable game:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;6x6 board with 3 ships&lt;/li&gt;
&lt;li&gt;AI with hunt/target algorithm&lt;/li&gt;
&lt;li&gt;Animations, ranking, match history.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The game looked great. Screens with dark naval aesthetics, Orbitron fonts, navy blue gradients with gold accents. Tutorial, configurable difficulty, internationalization in 3 languages.&lt;/p&gt;

&lt;p&gt;But it was a normal game. No ZK. No blockchain. Nothing that justified the hackathon.&lt;/p&gt;

&lt;p&gt;Looking back, the project had 4 clear phases:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Period&lt;/th&gt;
&lt;th&gt;Focus&lt;/th&gt;
&lt;th&gt;Commits&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Feb 07-08&lt;/td&gt;
&lt;td&gt;Complete single-player game&lt;/td&gt;
&lt;td&gt;107&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feb 14-15&lt;/td&gt;
&lt;td&gt;Research: ZK, Noir, Stellar Protocol&lt;/td&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feb 21-22&lt;/td&gt;
&lt;td&gt;ZK circuits + PvP backend&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feb 23&lt;/td&gt;
&lt;td&gt;Backend, frontend, and Soroban deploy on testnet&lt;/td&gt;
&lt;td&gt;112&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;February 23rd deserves an asterisk. 112 commits in a single day. It was the day everything needed to work together: ZK Proofs generated on the client, verified on the backend, submitted to the blockchain, with two real players facing each other via WebSocket online.&lt;/p&gt;

&lt;p&gt;Each fix led to another bug, which led to another discovery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GOOD TIMES GUARANTEED.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But I'm getting ahead of the story.&lt;/p&gt;




&lt;h2&gt;
  
  
  To be continued...
&lt;/h2&gt;

&lt;p&gt;In the next articles, I'll tell each phase with real code, real mistakes, and the "eureka" moments that made the project work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Part 2&lt;/strong&gt;: Writing ZK Proofs in Noir and discovering that the mobile app can't run ZK Proofs (bb.js)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 3&lt;/strong&gt;: Verifying proofs on the Stellar blockchain and spending 214 XLM chasing a bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 4&lt;/strong&gt;: Connecting two players in real time and the avalanche of race conditions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 5&lt;/strong&gt;: The lesson that cost 214 XLM — what I learned and would do differently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've never worked with ZK, blockchain, or games together — well, neither had I. Let's go.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Next: Part 2 — "Proving without Proofs"&lt;/em&gt;&lt;/p&gt;

</description>
      <category>stellarchallenge</category>
      <category>soroban</category>
      <category>hackathon</category>
      <category>zeroknowledge</category>
    </item>
  </channel>
</rss>
