<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Matthias Görgens</title>
    <subtitle>Notes on OCaml, Rust, property-based testing, zero-knowledge VMs, and breaking the occasional cipher.</subtitle>
    <link rel="self" type="application/atom+xml" href="https://paquari.com/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://paquari.com"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-07-16T00:00:00+00:00</updated>
    <id>https://paquari.com/atom.xml</id>
    <entry xml:lang="en">
        <title>Your generators already know how to shrink</title>
        <published>2026-07-16T00:00:00+00:00</published>
        <updated>2026-07-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://paquari.com/posts/choice-tapes/"/>
        <id>https://paquari.com/posts/choice-tapes/</id>
        
        <content type="html" xml:base="https://paquari.com/posts/choice-tapes/">&lt;p&gt;Here is a line from base_quickcheck that surprised me:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ocaml&quot; style=&quot;background-color:#ffffff;color:#323232;&quot; class=&quot;language-ocaml &quot;&gt;&lt;code class=&quot;language-ocaml&quot; data-lang=&quot;ocaml&quot;&gt;&lt;span style=&quot;font-style:italic;color:#969896;&quot;&gt;(* shrinker.ml *)
&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;let &lt;&#x2F;span&gt;&lt;span style=&quot;color:#0086b3;&quot;&gt;int &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; atomic
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;atomic&lt;&#x2F;code&gt; means “never produce any shrink candidate”. The same goes for
&lt;code&gt;int32&lt;&#x2F;code&gt;, &lt;code&gt;int64&lt;&#x2F;code&gt;, &lt;code&gt;float&lt;&#x2F;code&gt;, &lt;code&gt;char&lt;&#x2F;code&gt;, and &lt;code&gt;bool&lt;&#x2F;code&gt;. When your property
fails on &lt;code&gt;766135&lt;&#x2F;code&gt;, base_quickcheck reports &lt;code&gt;766135&lt;&#x2F;code&gt;. Only structure
shrinks: lists drop elements, but the elements themselves stay
whatever they were.&lt;&#x2F;p&gt;
&lt;p&gt;I do not think this is a bug. I think it is a principled surrender.
A &lt;code&gt;Shrinker.t&lt;&#x2F;code&gt; is a function from a value to smaller candidate values,
and it cannot know what the generator that produced the value would
have been willing to produce. Shrink an even number by halving and you
may hand an odd number to a test that assumed evenness; shrink a field
of a struct and you may violate an invariant the generator carefully
established. Jane Street chose the safe corner of a bad trade-off:
no shrinking rather than invalid shrinking.&lt;&#x2F;p&gt;
&lt;p&gt;The rest of the OCaml ecosystem took the other well-known exit.
QCheck2 and Bam both use integrated shrinking in the Hedgehog style:
a generator produces a lazy rose tree of values, and combinators
transform whole trees, so shrinking composes through &lt;code&gt;map&lt;&#x2F;code&gt; and
&lt;code&gt;filter&lt;&#x2F;code&gt; for free. It is a real improvement, and it still has a known
soft spot: monadic bind. When the outer value shrinks, the inner tree
was built from a value that no longer exists, and the usual answers
(regenerate, or freeze the outer value) both lose.&lt;&#x2F;p&gt;
&lt;p&gt;There is a third model, and as far as I can tell nobody had brought it
to OCaml: Hypothesis’s &lt;a href=&quot;https:&#x2F;&#x2F;hypothesis.works&#x2F;articles&#x2F;how-hypothesis-works&#x2F;&quot;&gt;Conjecture
engine&lt;&#x2F;a&gt;.
Record every random decision the generator makes as a typed choice. To
shrink, edit the recorded tape and run the generator again against it,
accepting the edit only if the test still fails and the new recording
is shorter or simpler. The property that makes this special: a shrink
proposal cannot violate a generator invariant, because the proposal is
not a value, it is an input to the generator. Whatever comes out went
through every filter, every smart constructor, every dependent bind,
exactly like the original.&lt;&#x2F;p&gt;
&lt;p&gt;I spent last week porting this model into &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;proptest-rs&#x2F;proptest&#x2F;pull&#x2F;658&quot;&gt;Rust’s
proptest&lt;&#x2F;a&gt;, which
required migrating strategies one by one to record typed choices. Then
I looked at base_quickcheck and realised something pleasing: OCaml
gets this almost for free.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;splittable-random-is-a-perfect-seam&quot;&gt;Splittable_random is a perfect seam&lt;&#x2F;h2&gt;
&lt;p&gt;Every base_quickcheck generator draws its randomness from one
sequential &lt;code&gt;Splittable_random.t&lt;&#x2F;code&gt;. And look at the primitives:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ocaml&quot; style=&quot;background-color:#ffffff;color:#323232;&quot; class=&quot;language-ocaml &quot;&gt;&lt;code class=&quot;language-ocaml&quot; data-lang=&quot;ocaml&quot;&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;val &lt;&#x2F;span&gt;&lt;span&gt;bool : &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;t &lt;&#x2F;span&gt;&lt;span&gt;-&amp;gt; &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;bool
&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;val &lt;&#x2F;span&gt;&lt;span&gt;int : &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;t &lt;&#x2F;span&gt;&lt;span&gt;-&amp;gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#63a35c;&quot;&gt;lo&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;int &lt;&#x2F;span&gt;&lt;span&gt;-&amp;gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#63a35c;&quot;&gt;hi&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;int &lt;&#x2F;span&gt;&lt;span&gt;-&amp;gt; &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;int
&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;val &lt;&#x2F;span&gt;&lt;span&gt;float : &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;t &lt;&#x2F;span&gt;&lt;span&gt;-&amp;gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#63a35c;&quot;&gt;lo&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;float &lt;&#x2F;span&gt;&lt;span&gt;-&amp;gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color:#63a35c;&quot;&gt;hi&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;float &lt;&#x2F;span&gt;&lt;span&gt;-&amp;gt; &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;float
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Every draw arrives with its bounds attached. In proptest I had to
migrate each strategy so the engine would know the type and range of
each decision; here the seam is typed already. Wrap these dozen
functions so they record to (and replay from) a tape when one is
installed, and every existing generator participates, including
everything &lt;code&gt;[@@deriving quickcheck]&lt;&#x2F;code&gt; produces. I verified the claim on
a derived record type: one generation records 89 typed choices, and
replaying that tape under a completely different RNG seed reproduces
the identical value. The vendored base_quickcheck in my repo is
byte-for-byte unmodified; a dune workspace resolves the
&lt;code&gt;splittable_random&lt;&#x2F;code&gt; library name to my shim, and that is the entire
integration.&lt;&#x2F;p&gt;
&lt;p&gt;(An earlier draft of this post said that &lt;code&gt;Generator.fn&lt;&#x2F;code&gt;’s randomly
generated functions do not shrink, because &lt;code&gt;fn&lt;&#x2F;code&gt; splits the random
state and split-off streams were not taped. That is no longer true:
the tape now keys sub-streams by the split identity and the
argument’s hash, so generated functions shrink too, down to
boundary-exact observed behaviour, and the reported counterexample’s
function stays callable after the run. That deserves, and will get,
its own post.)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;does-it-work&quot;&gt;Does it work?&lt;&#x2F;h2&gt;
&lt;p&gt;Six properties, 100 seeds each, identical failing examples handed to
both shrinkers. “Stock” is base_quickcheck’s own greedy loop, exactly
as &lt;code&gt;Test.run&lt;&#x2F;code&gt; performs it.&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;property&lt;&#x2F;th&gt;&lt;th&gt;stock minimal&lt;&#x2F;th&gt;&lt;th&gt;tape minimal&lt;&#x2F;th&gt;&lt;th&gt;tape avg calls&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;int uniform, fail iff &amp;gt;= 123457&lt;&#x2F;td&gt;&lt;td&gt;0&#x2F;100 (worst &lt;code&gt;766135&lt;&#x2F;code&gt;)&lt;&#x2F;td&gt;&lt;td&gt;100&#x2F;100&lt;&#x2F;td&gt;&lt;td&gt;38&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;pair, fail iff a + b &amp;gt;= 100&lt;&#x2F;td&gt;&lt;td&gt;0&#x2F;100 (worst &lt;code&gt;(481 781)&lt;&#x2F;code&gt;)&lt;&#x2F;td&gt;&lt;td&gt;100&#x2F;100&lt;&#x2F;td&gt;&lt;td&gt;22&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;list, fail iff length &amp;gt;= 3&lt;&#x2F;td&gt;&lt;td&gt;0&#x2F;100 (worst &lt;code&gt;(12 100 61)&lt;&#x2F;code&gt;)&lt;&#x2F;td&gt;&lt;td&gt;100&#x2F;100&lt;&#x2F;td&gt;&lt;td&gt;466&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;list, fail iff sum &amp;gt;= 100&lt;&#x2F;td&gt;&lt;td&gt;0&#x2F;100 (worst &lt;code&gt;(15 91)&lt;&#x2F;code&gt;)&lt;&#x2F;td&gt;&lt;td&gt;100&#x2F;100&lt;&#x2F;td&gt;&lt;td&gt;98&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;filtered evens, fail iff &amp;gt;= 100&lt;&#x2F;td&gt;&lt;td&gt;0&#x2F;100 (worst &lt;code&gt;21150&lt;&#x2F;code&gt;)&lt;&#x2F;td&gt;&lt;td&gt;100&#x2F;100&lt;&#x2F;td&gt;&lt;td&gt;91&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;bind: length-prefixed list, sum &amp;gt;= 100&lt;&#x2F;td&gt;&lt;td&gt;0&#x2F;100 (a 64-element monster)&lt;&#x2F;td&gt;&lt;td&gt;100&#x2F;100&lt;&#x2F;td&gt;&lt;td&gt;49&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;The bind row is the point. &lt;code&gt;let%bind len = ... in list_with_length ~length:len ...&lt;&#x2F;code&gt; has no derivable shrinker at all, so stock reports
whatever it generated. The tape engine returns &lt;code&gt;[100]&lt;&#x2F;code&gt;, the global
minimum, in 49 test executions on average: it lowers the length choice
while deleting the choices of one element, replays, and the generator
rebuilds a consistent shorter list every time.&lt;&#x2F;p&gt;
&lt;p&gt;My favourite single number is from a three-way chained bind, &lt;code&gt;a&lt;&#x2F;code&gt; in
10..1000, &lt;code&gt;b&lt;&#x2F;code&gt; in 10..a, &lt;code&gt;c&lt;&#x2F;code&gt; in 10..b, failing unconditionally. The
tape engine’s first proposal sets every choice to its target; replay
walks the binds again, so the dependency structure holds by
construction, and it lands on &lt;code&gt;(10, 10, 10)&lt;&#x2F;code&gt; in one attempt.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-the-tape-sees-inside-base-quickcheck&quot;&gt;What the tape sees inside base_quickcheck&lt;&#x2F;h2&gt;
&lt;p&gt;Recording every draw gives you an X-ray of generator internals, and
two findings seem worth reporting upstream.&lt;&#x2F;p&gt;
&lt;p&gt;First, &lt;code&gt;Generator.int_inclusive&lt;&#x2F;code&gt; is a weighted union: 5% &lt;code&gt;return lo&lt;&#x2F;code&gt;,
5% &lt;code&gt;return hi&lt;&#x2F;code&gt;, 90% uniform. The constant branches record a tape of
one choice (the branch selector), and escaping to the shrinkable
uniform branch would lengthen the tape, which a shortlex-ordered
search must refuse. So one failing case in ten starts inside a trap.
Hypothesis structures the same boundary bias inside the sampler, one
typed choice with a biased distribution, and every case is
shrinkable. Distributional bias belongs in the draw, not in generator
structure. This is a small, concrete change to suggest.&lt;&#x2F;p&gt;
&lt;p&gt;Second, one list element is not one choice. &lt;code&gt;list_generic&lt;&#x2F;code&gt; draws a
length, a size-budget distribution, a permutation, then elements, so
removing an element means deleting a shuffle draw and a value draw
together. My deletion pass learned to remove small contiguous blocks
alongside lowering the length. This is worth knowing for anyone
building a coverage-guided fuzzer on top of these generators, too.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-oxcaml-part&quot;&gt;The OxCaml part&lt;&#x2F;h2&gt;
&lt;p&gt;The engine builds and runs under OxCaml (the 5.2.0+ox overlay, Base
and ppxlib at v0.18 preview) with a handful of compatibility shims in
my vendored copies, none in the engine. Same benchmark, both
compilers: byte-identical results, down to the same 10129 total shrink
attempts across 200 runs. For a testing tool, determinism across
compiler forks is a feature worth stating.&lt;&#x2F;p&gt;
&lt;p&gt;Shrink attempts are also embarrassingly parallel: independent replays
of edited tapes, racing to find an accepted improvement. My first
engine had exactly one piece of state in the way, a convenient global
the shim consulted, and OxCaml’s mode checker rejected it by
inference, with a paper trail, before any parallelism existed to go
wrong. The refactor it forced bought a 4.6x wall-clock win the same
afternoon (and Flambda2 runs the sequential engine 12 percent faster
while we are at it). That story, with the compiler’s actual review
comments, is &lt;a href=&quot;https:&#x2F;&#x2F;paquari.com&#x2F;posts&#x2F;mode-checker-review&#x2F;&quot;&gt;the next post&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where-this-could-go&quot;&gt;Where this could go&lt;&#x2F;h2&gt;
&lt;p&gt;The &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;matthiasgoergens&#x2F;tapecheck&quot;&gt;repo&lt;&#x2F;a&gt; has a
drop-in &lt;code&gt;Tape_test&lt;&#x2F;code&gt; module mirroring
&lt;code&gt;Base_quickcheck.Test.run&#x2F;run_exn&#x2F;result&lt;&#x2F;code&gt;; existing suites switch by
renaming one module, and the &lt;code&gt;quickcheck_shrinker&lt;&#x2F;code&gt; they already
declare is simply ignored. The honest upstream path is small: tape
hooks in splittable_random’s dozen primitives, behind a no-op default
(&lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;janestreet&#x2F;splittable_random&#x2F;pull&#x2F;2&quot;&gt;proposed&lt;&#x2F;a&gt;).
And a typed tape is a better fuzzing corpus than raw bytes, so there
is an AFL++ custom-mutator story here too, but that is a later post.&lt;&#x2F;p&gt;
&lt;p&gt;If you maintain OCaml property tests and have a failing case that
reported a 64-element monster where a one-element list would do, I
would love to hear whether this engine finds it.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>The mode checker reviewed my code</title>
        <published>2026-07-16T00:00:00+00:00</published>
        <updated>2026-07-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://paquari.com/posts/mode-checker-review/"/>
        <id>https://paquari.com/posts/mode-checker-review/</id>
        
        <content type="html" xml:base="https://paquari.com/posts/mode-checker-review/">&lt;p&gt;My property-testing engine for base_quickcheck had a global. I knew it
was there. It was the convenient kind of global: a single
&lt;code&gt;Tape.t option ref&lt;&#x2F;code&gt; that the random-state shim consulted on every
draw, so the engine could install a recording tape without threading
it through any signatures. It worked, all my tests passed, and I had
already written the comment apologising for it.&lt;&#x2F;p&gt;
&lt;p&gt;Then I wanted parallel shrinking. Shrink attempts are embarrassingly
parallel in a choice-tape engine: each attempt replays an edited tape
through the generator, needing nothing from any other attempt. The
obvious question for OxCaml was whether the mode system would let me
say that. So I asked it directly, by claiming that installing a tape
was portable, the mode meaning roughly “safe to use from another
domain”:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ocaml&quot; style=&quot;background-color:#ffffff;color:#323232;&quot; class=&quot;language-ocaml &quot;&gt;&lt;code class=&quot;language-ocaml&quot; data-lang=&quot;ocaml&quot;&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;let &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#795da3;&quot;&gt;probe &lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;(unit -&amp;gt; unit) Modes.Portable.t =
&lt;&#x2F;span&gt;&lt;span&gt;  { portable &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;fun &lt;&#x2F;span&gt;&lt;span&gt;() -&amp;gt; Splittable_random.For_tape.set_tape None) }
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The compiler said no, and it said no with a paper trail:&lt;&#x2F;p&gt;
&lt;pre style=&quot;background-color:#ffffff;color:#323232;&quot;&gt;&lt;code&gt;&lt;span&gt;Error: The value &amp;quot;Splittable_random.For_tape.set_tape&amp;quot; is &amp;quot;nonportable&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;       but is expected to be &amp;quot;portable&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;         because it is used inside the function at line 7
&lt;&#x2F;span&gt;&lt;span&gt;         which is expected to be &amp;quot;portable&amp;quot;
&lt;&#x2F;span&gt;&lt;span&gt;         because it is the field &amp;quot;portable&amp;quot; (with some modality) of
&lt;&#x2F;span&gt;&lt;span&gt;         the record at line 7.
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;No annotations anywhere in my code. The checker inferred, from the
definition alone, that a function closing over a global mutable ref
cannot be handed to another domain, and told me which value, in which
closure, violated which expectation, and why the expectation existed.
This is the code review comment a careful colleague would leave:
“this global will bite you the moment you go parallel”, except it is
machine-checked and cannot be argued with.&lt;&#x2F;p&gt;
&lt;p&gt;The fix the checker forces is the design a reviewer would have asked
for anyway: carry the tape inside the random state.&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;ocaml&quot; style=&quot;background-color:#ffffff;color:#323232;&quot; class=&quot;language-ocaml &quot;&gt;&lt;code class=&quot;language-ocaml&quot; data-lang=&quot;ocaml&quot;&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;type t &lt;&#x2F;span&gt;&lt;span&gt;=
&lt;&#x2F;span&gt;&lt;span&gt;  { real : Sr_real&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;.t
&lt;&#x2F;span&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;; &lt;&#x2F;span&gt;&lt;span&gt;tape : Tape&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;.t option
&lt;&#x2F;span&gt;&lt;span&gt;  }
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;let &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#795da3;&quot;&gt;attach &lt;&#x2F;span&gt;&lt;span&gt;t tape &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span&gt;{ t &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;with&lt;&#x2F;span&gt;&lt;span&gt; tape &lt;&#x2F;span&gt;&lt;span style=&quot;font-weight:bold;color:#a71d5d;&quot;&gt;= &lt;&#x2F;span&gt;&lt;span&gt;Some tape }
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;No global, no install&#x2F;uninstall dance, and every shrink attempt
becomes self-contained: its own tape, its own RNG state, nothing
shared. After the refactor the same probe, now building all of its
state inside the closure, compiles and runs. The before and after are
one commit apart, and the diff is the honest one: signatures now say
what the code always meant.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-compiler-had-a-second-opinion&quot;&gt;The compiler had a second opinion&lt;&#x2F;h2&gt;
&lt;p&gt;With attempts self-contained I reached for &lt;code&gt;Domain.spawn&lt;&#x2F;code&gt; to build a
worker pool, and OxCaml’s standard library had opinions about that
too:&lt;&#x2F;p&gt;
&lt;pre style=&quot;background-color:#ffffff;color:#323232;&quot;&gt;&lt;code&gt;&lt;span&gt;Alert do_not_spawn_domains: Stdlib.Domain.spawn
&lt;&#x2F;span&gt;&lt;span&gt;User programs should never spawn domains. [...] spawning more than
&lt;&#x2F;span&gt;&lt;span&gt;[recommended_domain_count] domains will significantly degrade GC
&lt;&#x2F;span&gt;&lt;span&gt;performance.
&lt;&#x2F;span&gt;&lt;span&gt;
&lt;&#x2F;span&gt;&lt;span&gt;Alert unsafe_multidomain: Stdlib.Domain.spawn
&lt;&#x2F;span&gt;&lt;span&gt;Use [Domain.Safe.spawn].
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;Domain.Safe.spawn&lt;&#x2F;code&gt; is the mode-checked variant: it demands a portable
closure, exactly the property the probe above established. The system
composes: the mode error pushed the state into the right place, and
the safe-spawn API is the payoff for having done it, a spawn that the
compiler can check does not smuggle shared mutable state across
domains. (My pool currently uses plain &lt;code&gt;Stdlib.Domain&lt;&#x2F;code&gt; so the same
source builds on stock OCaml; switching the OxCaml build to
&lt;code&gt;Domain.Safe&lt;&#x2F;code&gt; is the natural next step, and the alert will keep
nagging until I do.)&lt;&#x2F;p&gt;
&lt;p&gt;I want to dwell on what did NOT happen. I did not add mode annotations
to my engine. I did not port anything to a new concurrency framework.
I compiled existing code under a compiler with a stricter type system
and asked it one question, and it found the one piece of state that
made parallelism unsafe, explained itself, and pointed at the
sanctioned alternative. The cost of admission was zero; the review
was free.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;was-it-worth-it-the-numbers&quot;&gt;Was it worth it? The numbers&lt;&#x2F;h2&gt;
&lt;p&gt;The pool evaluates generation cases (find the failing example) and
deletion-scan proposals (shrink it) in parallel batches. Two details
matter for correctness: taking the lowest failing index in a
generation batch reproduces the sequential engine’s choice of failing
example exactly, and shrink acceptance still goes through one
shortlex comparison against the incumbent, so results stay
deterministic. Attempt counts are identical at every domain count.&lt;&#x2F;p&gt;
&lt;p&gt;Benchmark: a bind-heavy generator (length 1..256, then that many
bounded ints), a test body with a fixed ~100us of work, failures in
roughly one case in thirty, twenty trials. First on stock OCaml 5.3:&lt;&#x2F;p&gt;
&lt;pre style=&quot;background-color:#ffffff;color:#323232;&quot;&gt;&lt;code&gt;&lt;span&gt;domains= 1  wall 9.16s
&lt;&#x2F;span&gt;&lt;span&gt;domains= 4  wall 2.98s
&lt;&#x2F;span&gt;&lt;span&gt;domains= 8  wall 2.01s
&lt;&#x2F;span&gt;&lt;span&gt;domains=16  wall 1.92s
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;4.6x at 16 domains, saturating because shrinking’s bisection passes
are still sequential (Amdahl, as always). And the same binary-except-
compiler under OxCaml:&lt;&#x2F;p&gt;
&lt;pre style=&quot;background-color:#ffffff;color:#323232;&quot;&gt;&lt;code&gt;&lt;span&gt;domains= 1  wall 8.03s
&lt;&#x2F;span&gt;&lt;span&gt;domains= 8  wall 2.00s
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Flambda2 runs the sequential engine 12 percent faster out of the box,
and the parallel ceiling is the same. Identical results throughout:
same failures found, same 57000 shrink attempts, on both compilers at
every width.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-takeaway&quot;&gt;The takeaway&lt;&#x2F;h2&gt;
&lt;p&gt;The pitch for OxCaml’s modes is usually written from the perspective
of people building concurrent systems on purpose. My engine is a
humbler data point: a small library that was not written with
parallelism in mind, whose author knew about a shortcut and took it.
The mode checker found the shortcut from outside, produced the
refactor a good reviewer would have demanded, and the refactor paid
for itself the same afternoon with a 4.6x wall-clock win.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;postscript-the-checker-reviewed-the-upstream-pr-too&quot;&gt;Postscript: the checker reviewed the upstream PR too&lt;&#x2F;h2&gt;
&lt;p&gt;After this post was drafted, I turned the shim into a proposed
upstream patch: an &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;janestreet&#x2F;splittable_random&#x2F;pull&#x2F;2&quot;&gt;Intercept record carried inside
splittable_random’s state&lt;&#x2F;a&gt;,
so any engine can hook the draws. The mode checker had one more
review comment: a state carrying hook closures is nonportable DATA, so
my portable-attempt probe stopped typechecking the moment hooks moved
into the state. It is right again: hooks capture arbitrary state, and
whether the intercept field should demand portable hooks (data-race
freedom composes, but recording observers inherently capture mutable
state) or carry a nonportable modality is a real design decision. It
is the first question in the upstream proposal, and I find it
delightful that the compiler raised it before any human reviewer saw
the patch.&lt;&#x2F;p&gt;
&lt;p&gt;The engine itself, and the shrinking results that motivated all of
this, are the subject of &lt;a href=&quot;https:&#x2F;&#x2F;paquari.com&#x2F;posts&#x2F;choice-tapes&#x2F;&quot;&gt;the previous
post&lt;&#x2F;a&gt;. Code at
&lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;matthiasgoergens&#x2F;tapecheck&quot;&gt;github.com&#x2F;matthiasgoergens&#x2F;tapecheck&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>A proof that 0 = 1, in a real zk-VM</title>
        <published>2026-07-16T00:00:00+00:00</published>
        <updated>2026-07-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://paquari.com/posts/proof-of-0-equals-1/"/>
        <id>https://paquari.com/posts/proof-of-0-equals-1/</id>
        
        <content type="html" xml:base="https://paquari.com/posts/proof-of-0-equals-1/">&lt;p&gt;A zero-knowledge VM makes one promise to a verifier: &lt;em&gt;I ran the program
whose hash is H, on this input, and got this output, and here is a
proof you can check without re-running anything.&lt;&#x2F;em&gt; The whole edifice
rests on that hash pinning down exactly one program. In December 2022
I built a Miden Assembly program that outputs a stack of zeros,
together with a valid proof that it outputs a one. Same hash, both
true. A proof that 0 = 1.&lt;&#x2F;p&gt;
&lt;p&gt;I should say up front what my contribution was and was not. The Miden
team already knew the underlying weakness. It was written down, in a
&lt;code&gt;TODO&lt;&#x2F;code&gt; comment, in their own source. My contribution was to notice
that the comment described an exploitable soundness hole rather than a
tidiness issue, and to actually build the exploit. The gap between “we
know these two node types hash the same” and “here is a proof that
0 = 1” is the whole of this post, and it is smaller than it looks and
larger than it sounds.&lt;&#x2F;p&gt;
&lt;p&gt;Here is the program:&lt;&#x2F;p&gt;
&lt;pre style=&quot;background-color:#ffffff;color:#323232;&quot;&gt;&lt;code&gt;&lt;span&gt;begin
&lt;&#x2F;span&gt;&lt;span&gt;  if.true
&lt;&#x2F;span&gt;&lt;span&gt;    push.1
&lt;&#x2F;span&gt;&lt;span&gt;  end
&lt;&#x2F;span&gt;&lt;span&gt;end
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Run it and you get zeros. &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;matthiasgoergens&#x2F;miden-collision&quot;&gt;The
proof&lt;&#x2F;a&gt; says you
get a one. Neither the executor nor the verifier is buggy in
isolation; they simply disagree about which program the hash refers
to, and that gap is the whole exploit.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;programs-are-trees-and-the-tree-is-the-identity&quot;&gt;Programs are trees, and the tree is the identity&lt;&#x2F;h2&gt;
&lt;p&gt;Miden does not hash the text of your assembly. It compiles the program
to a Merkelised abstract syntax tree (a MAST), and the program’s
identity is the hash of the tree’s root. Two node types matter here:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SPLIT(a, b)&lt;&#x2F;code&gt; is a conditional. It reads the top of the stack; if it
is one, it runs &lt;code&gt;a&lt;&#x2F;code&gt;, and if it is zero, it runs &lt;code&gt;b&lt;&#x2F;code&gt;. The &lt;code&gt;if.true&lt;&#x2F;code&gt;
above compiles to a &lt;code&gt;SPLIT&lt;&#x2F;code&gt; whose true branch is &lt;code&gt;push.1&lt;&#x2F;code&gt; and whose
false branch is empty. On the default stack of zeros, &lt;code&gt;SPLIT&lt;&#x2F;code&gt; takes
the false branch and does nothing. Output: zeros.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;JOIN(a, b)&lt;&#x2F;code&gt; is a sequence. It runs &lt;code&gt;a&lt;&#x2F;code&gt;, then runs &lt;code&gt;b&lt;&#x2F;code&gt;,
unconditionally.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Now the flaw. A &lt;code&gt;SPLIT&lt;&#x2F;code&gt; node and a &lt;code&gt;JOIN&lt;&#x2F;code&gt; node with the same two
children hashed to the &lt;em&gt;same value&lt;&#x2F;em&gt;. The hash was
&lt;code&gt;Hash(child_a, child_b)&lt;&#x2F;code&gt; with nothing to say which node type it was.
This was not hidden. The comment above the &lt;code&gt;Join&lt;&#x2F;code&gt; block’s hashing code
said so in as many words:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; style=&quot;background-color:#ffffff;color:#323232;&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&lt;span style=&quot;font-style:italic;color:#969896;&quot;&gt;&#x2F;&#x2F;&#x2F; Hash of a Join block is computed by hashing a concatenation of the
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#969896;&quot;&gt;&#x2F;&#x2F;&#x2F; hashes of joined blocks.
&lt;&#x2F;span&gt;&lt;span style=&quot;font-style:italic;color:#969896;&quot;&gt;&#x2F;&#x2F;&#x2F; TODO: update hashing methodology to make it different from Split block.
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;A &lt;code&gt;TODO&lt;&#x2F;code&gt; is a note to future maintainers that a thing is unfinished. It
is easy to read this one as a matter of neatness. What it actually
describes is that two different programs share a hash, and in a system
whose entire security argument is “the hash identifies the program”,
that is soundness. So these two trees are indistinguishable by hash:&lt;&#x2F;p&gt;
&lt;pre style=&quot;background-color:#ffffff;color:#323232;&quot;&gt;&lt;code&gt;&lt;span&gt;SPLIT(SPAN push.1, SPAN noop)      # the honest if.true, outputs 0
&lt;&#x2F;span&gt;&lt;span&gt;JOIN(SPAN push.1, SPAN noop)       # runs push.1 then noop, outputs 1
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The &lt;code&gt;JOIN&lt;&#x2F;code&gt; version runs &lt;code&gt;push.1&lt;&#x2F;code&gt; and then the no-op, and leaves a one
on top of the stack. It has the same hash as the honest conditional.
So I proved the execution of the &lt;code&gt;JOIN&lt;&#x2F;code&gt; tree, honestly, and handed the
verifier a proof whose program hash matches the &lt;code&gt;SPLIT&lt;&#x2F;code&gt; tree that any
normal user would have written. The verifier checks the proof, checks
that the hash is the one it expected, and concludes that the
zero-outputting program output a one.&lt;&#x2F;p&gt;
&lt;p&gt;The one wrinkle was construction. The Miden Assembly compiler never
emits the &lt;code&gt;JOIN&lt;&#x2F;code&gt; variant, because &lt;code&gt;if.true&lt;&#x2F;code&gt; always lowers to &lt;code&gt;SPLIT&lt;&#x2F;code&gt;.
So there is no source program that produces the malicious tree; I had
to build the MAST by hand, which meant hacking up the compiler to let
me assemble the collided tree directly. The hack was not pretty, and I
said so in the report.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;reporting-it-in-the-zero-knowledge-spirit&quot;&gt;Reporting it in the zero-knowledge spirit&lt;&#x2F;h2&gt;
&lt;p&gt;I filed the issue as a teaser: here is a program that outputs zeros,
here is a proof it outputs a one, the proof is in this repo, have fun.
No explanation of the mechanism. It seemed only fitting to report a
zero-knowledge bug with a zero-knowledge disclosure, and since the team
already knew about the underlying &lt;code&gt;TODO&lt;&#x2F;code&gt;, the fun part was watching
them connect it to a concrete exploit.&lt;&#x2F;p&gt;
&lt;p&gt;Bobbin Threadbare, who leads Miden, did exactly that within a day:
“my guess is that you constructed two Miden VM programs which do
different things but hash to the same value … &lt;code&gt;JOIN&lt;&#x2F;code&gt; and &lt;code&gt;SPLIT&lt;&#x2F;code&gt;
blocks are hashed in exactly the same way,” with a link straight to
the &lt;code&gt;TODO&lt;&#x2F;code&gt;. Exactly right. I confirmed it, and added the thing that
worried me more than this single instance: the code kept picturing an
attacker who crafts malicious &lt;em&gt;source programs&lt;&#x2F;em&gt;, when a real attacker
works one level down, at the tree the source compiles to. Wherever
that assumption hid, there were probably more collisions like this one.
The team agreed, and treated the whole thing as the fun collaborative
puzzle it was.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fixing-a-collision-without-paying-for-it&quot;&gt;Fixing a collision without paying for it&lt;&#x2F;h2&gt;
&lt;p&gt;The fix is conceptually easy: make different node types hash to
different values. The interesting part is doing it inside an
arithmetic circuit, where every extra operation is a real cost
multiplied across every instruction, and where the hash function has a
fixed number of input slots you have already spent.&lt;&#x2F;p&gt;
&lt;p&gt;The thread turned into a small design discussion of the options, and
Edward Kmett showed up with the cleanest of the arithmetic ones. Rather
than widening the hash to take an extra “which node type” input (there
were no free slots), apply a different affine transformation to one of
the hash inputs per node type: hash &lt;code&gt;3a + 1, b&lt;&#x2F;code&gt; for one kind and
&lt;code&gt;5a + 2, b&lt;&#x2F;code&gt; for another. Distinct linear adjustments send otherwise
-equal inputs to unrelated outputs, so all tree shapes hash apart, at a
cost of one multiply and one add per node against the full price of a
cryptographic permutation. He preferred an affine map over a bare
multiply precisely because one of the real hash inputs was zero, and a
constant times zero is still zero. He even put a fix over the fence as
a PR, mostly to unblock his own team while the discussion continued.&lt;&#x2F;p&gt;
&lt;p&gt;The other candidates each had a catch at the time. Domain separation,
setting a reserved capacity register of the sponge to a per-node-type
value, is the simplest and cheapest idea, but the sponge’s capacity is
also its security margin, and the hash function then in use had none to
spare. Using different output slots as the digest costs no field
operations but complicates the constraint system.&lt;&#x2F;p&gt;
&lt;p&gt;Here is the part I did not see coming. Miden was already migrating to a
new hash function, RPO (Rescue Prime Optimised), and that migration
changed the arithmetic of the decision. RPO’s wider state left a
capacity register genuinely free, so the objection to domain
separation, the one thing that had made it look expensive, simply
evaporated. &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;0xMiden&#x2F;miden-vm&#x2F;pull&#x2F;682&quot;&gt;The fix they
shipped&lt;&#x2F;a&gt; is not Kmett’s
affine transform; it writes a domain specifier into the second capacity
element when hashing a control block, composed from the block’s opcode
bits.&lt;&#x2F;p&gt;
&lt;p&gt;I still find the affine mixer the prettier answer, for a reason that
outlives this particular bug. If your hash is collision-resistant, it
already sends linearly-adjusted inputs to unrelated outputs, so a
distinct multiplier per node type gives you a distinct domain, and you
can keep minting them: a fresh prime for every new node type you ever
add, unboundedly many, at the price of one multiply and add and no
state at all. Domain separation by capacity register spends a scarce
resource, the sponge state that is also your security margin, and caps
the number of domains at the bits you are willing to give up. One
technique scales with the algebra of the hash; the other rations a
fixed budget.&lt;&#x2F;p&gt;
&lt;p&gt;That said, I will not fault Miden’s choice, and I might not be weighing
every trade-off they were. Once RPO handed them a free register, domain
separation was zero marginal cost against the work already in flight,
the constraint-system impact was theirs to live with, and shipping the
simple thing that closes the hole beats holding out for the elegant
thing. The cheapest option in principle became the cheapest option in
practice once a different piece of work moved the constraints. It is a
good reminder that the right fix depends on the state of the whole
system, not just the local problem, and that a design thread is worth
having even when the answer changes out from under it.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-this-genre-of-bug-is-worth-staring-at&quot;&gt;Why this genre of bug is worth staring at&lt;&#x2F;h2&gt;
&lt;p&gt;The Miden executor was correct. The verifier was correct. The hash
function was a fine hash function. Every component did what it said,
and the system as a whole would happily certify that zero equals one,
because two components disagreed about the meaning of a single hash and
nothing forced them to agree.&lt;&#x2F;p&gt;
&lt;p&gt;That is the shape of most soundness bugs I have seen in
proof systems, and it is why they reward a particular habit: stop
reading the source language and start reading the object the source
compiles to, the thing the proof actually talks about. The MASM
programmer cannot write the malicious tree. The person holding the
MAST can. A zero-knowledge proof is only as honest as the
injectivity of the map from programs to hashes, and that map is easy
to build with an accidental collision hiding in it, even, as here,
one the authors had already flagged.&lt;&#x2F;p&gt;
&lt;p&gt;None of this is a knock on Miden. Writing down “this is not yet safe”
in the code is exactly what a careful team does with a known gap, and
they fixed it properly rather than papering over my particular
witness. The value I added was small and specific: turning a line of
self-aware &lt;code&gt;TODO&lt;&#x2F;code&gt; into a running artefact that makes the cost of
leaving it undone impossible to misjudge. That is often the most
useful thing an outsider can do for a serious project, and the Miden
folks were a pleasure to do it with.&lt;&#x2F;p&gt;
&lt;p&gt;The full artefact, including the Dockerised proof you can run
yourself, is &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;matthiasgoergens&#x2F;miden-collision&quot;&gt;on GitHub&lt;&#x2F;a&gt;;
the original report and the design discussion are &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;0xMiden&#x2F;miden-vm&#x2F;issues&#x2F;605&quot;&gt;Miden VM issue
605&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>A solution in search of a problem</title>
        <published>2026-07-16T00:00:00+00:00</published>
        <updated>2026-07-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://paquari.com/posts/solution-in-search-of-a-problem/"/>
        <id>https://paquari.com/posts/solution-in-search-of-a-problem/</id>
        
        <content type="html" xml:base="https://paquari.com/posts/solution-in-search-of-a-problem/">&lt;p&gt;A few years ago I was CTO of Mozak, and we built a RISC-V zkVM: write
Rust (or anything that compiles to RISC-V), run it, get a
zero-knowledge proof that it ran correctly. The company is dormant
now, and some of our best design ideas were never written up. This
post starts paying down that debt with the one I had the most trouble
explaining while it mattered: how independent programs coordinate.&lt;&#x2F;p&gt;
&lt;p&gt;The trouble was not that people found the design wrong. It was worse:
I repeatedly failed to convince smart people in the field that it
solved a problem anyone had. At some point I started calling it “a
solution in search of a problem” myself. I now think I know exactly
why the conversations went that way, and the diagnosis is the most
interesting part, so this post tells the story in that order: the
idea, the failure to sell it, the diagnosis.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;programs-that-cannot-speak&quot;&gt;Programs that cannot speak&lt;&#x2F;h2&gt;
&lt;p&gt;Strictly speaking, a zero-knowledge proof cannot produce output. It
can only accept or reject input. In a non-deterministic setting that
turns out to be enough: instead of &lt;em&gt;emitting&lt;&#x2F;em&gt; a value, a program is
handed a candidate value as advice and checks “yes — this is what I
would have said” (or fails). Output becomes validated input. Every
zkVM leans on this trick somewhere.&lt;&#x2F;p&gt;
&lt;p&gt;Our idea was to play the same trick &lt;em&gt;between&lt;&#x2F;em&gt; programs. Present every
program participating in a joint action — a token transfer, say —
with a shared &lt;strong&gt;script&lt;&#x2F;strong&gt;: an opaque record of the intended dialogue.
Who called whom, with what arguments, what came back. A program
accepting the script means:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;If the others say what the script says they say, then I approve of
what the script says I do.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Like two parties signing a contract a lawyer drafted for them, or
diplomats initialling a protocol an aide prepared. Each signs only for
their own part; none needs to read the others’ private reasoning. No
call ever happens between programs. Each performs its part alone, in
its own proof, and the parts line up because each was validated
against the same script.&lt;&#x2F;p&gt;
&lt;p&gt;To the proof system the script is a black box of bytes — its meaning
is whatever convention the programs share. One thing, though, is
verified rather than conventional: the &lt;strong&gt;cast&lt;&#x2F;strong&gt;, the ordered list of
programs party to the script, each named by its code hash. The
verifier checks each proof, checks every seat in the cast is matched
by a proof of exactly that program at exactly that position, and
checks everyone was bound to the same script. It never reads what
anyone said.&lt;&#x2F;p&gt;
&lt;p&gt;So: a token program insists transfers conserve tokens and that the
relevant wallets agree; a wallet approves by proving its owner signed
off; each is a black box to the other; and a transaction settles when
all the proofs land and the cast checks out.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-not-just-verify-the-proof&quot;&gt;“Why not just verify the proof?”&lt;&#x2F;h2&gt;
&lt;p&gt;Whenever I pitched this, the counter-proposal was always the same,
and always reasonable: have the token program &lt;em&gt;verify the wallet’s
proof&lt;&#x2F;em&gt; inside its own execution. Proof recursion. Every major zkVM
supports it, and it is the answer the field reaches for by reflex.&lt;&#x2F;p&gt;
&lt;p&gt;Recursion is sound, and for its intended job — compressing many
proofs into one — we used it too. But as a &lt;em&gt;coordination&lt;&#x2F;em&gt; mechanism
it has a closed-world assumption baked in: to verify the wallet’s
proof, the token program must name the wallet’s verification key &lt;em&gt;at
authoring time&lt;&#x2F;em&gt;. Now run the scenario that actually motivated us. A
corporate wallet requires CEO and CTO approval, or a sign-off from
compliance above some threshold — private, changing rules. Years
after the token shipped, a regulator requires a screening attestation
from an accredited provider for large transfers. Under recursion the
token program would have needed the compliance provider’s
verification key baked in before that provider existed. Nobody can be
added to a transaction who was not foreseen by the programs already
in it, and some program must sit at the root of the verification tree
and see everyone.&lt;&#x2F;p&gt;
&lt;p&gt;In the script model the compliance program just joins the cast. The
wallet’s policy demands its presence; the token program neither knows
nor cares — with separate scripts it need not even learn compliance
was involved. The participant set is decided per transaction, not
frozen per program.&lt;&#x2F;p&gt;
&lt;p&gt;I found this argument completely convincing. The people I pitched it
to mostly did not. They kept asking, gently, when a set of programs
that genuinely do not trust each other, cannot read each other, and
were written in ignorance of each other would ever need to agree on
one atomic action. Fair question. Here is the answer I wish I had
had.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-one-program-world&quot;&gt;The one-program world&lt;&#x2F;h2&gt;
&lt;p&gt;Almost everyone building zk systems is, at the proof level, running
exactly one program: an interpreter. On Ethereum rollups it is the
EVM; elsewhere it is some other state-transition function. The
contracts people actually write live one storey up, as &lt;em&gt;data&lt;&#x2F;em&gt; that
the interpreter executes. And at that level, contracts are not black
boxes at all. The interpreter holds all their bytecode, executes
every one of them, mediates every call, owns all the shared state.
Cross-program coordination is solved by ordinary interpreter
mechanics — transparently, by a single executor that sees everything.
The proof at the bottom certifies one sentence: &lt;em&gt;the interpreter ran
correctly&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;If that is your world, my problem statement is unintelligible.
Mutually-distrusting programs? They all trust the interpreter.
Opaque counterparties? The executor reads every contract. Unforeseen
third parties? Deploy another contract; the interpreter mediates. The
problem does not exist — because the architecture bought its absence,
and paid with a single all-seeing executor, contracts that are public
to it, one monolithic execution trace, and the standing cost of
proving an interpreter interpreting instead of programs running.&lt;&#x2F;p&gt;
&lt;p&gt;Mozak made the opposite choice: no interpreter. Contracts are
zkVM-native programs, each proven directly, by whoever wants to prove
it. The moment you make that choice you &lt;em&gt;have no executor to mediate
calls&lt;&#x2F;em&gt; — so you are forced to invent coordination that isolated,
independently-generated proofs can each check alone. That is the
script. The design was never a feature we bolted on; it is what
remains of “A calls B” after you delete the layer that was doing the
calling.&lt;&#x2F;p&gt;
&lt;p&gt;Distributed systems has a name for this distinction. &lt;em&gt;Orchestration&lt;&#x2F;em&gt;:
a central conductor holds the logic and tells every participant what
to do — the EVM, exactly. &lt;em&gt;Choreography&lt;&#x2F;em&gt;: no conductor; each
participant knows its own part and stays in step by following a plan
agreed in advance. Programs on Mozak are choreographed. (The theatre
metaphor runs deeper — the participant list was literally called the
&lt;code&gt;cast_list&lt;&#x2F;code&gt; in our code — and the precise mechanics of scripts,
seats, and settlement deserve their own post.)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-refusing-the-interpreter-buys&quot;&gt;What refusing the interpreter buys&lt;&#x2F;h2&gt;
&lt;p&gt;Everything follows from the one move. Nobody holds all the programs,
so different distrusting parties prove different programs separately
— a hardware signer can produce the wallet’s approval offline on
Monday and a relayer can assemble the settlement on Friday without
ever touching a key. Nobody &lt;em&gt;sees&lt;&#x2F;em&gt; all the programs, so counterparties
stay opaque: a program reveals neither its code nor its private
inputs, only what the script says. Mediation lives in a document
rather than an executor, so the participant set stays open — the
compliance program nobody foresaw joins by appearing in a cast. And
there is no interpreter in the circuit to pay for: programs compile
to the machine’s own instruction set and are proven directly.&lt;&#x2F;p&gt;
&lt;p&gt;The honest comparison, briefly. Recursion-as-coordination assumes the
relationships are known at authoring time — precisely when you do not
have this problem. Monolithic proving (zkEVMs, Cairo) assumes one
trusted executor and public contracts. The nearest cousin is Aleo,
which also proves calls as separate transitions bound into a
transaction — and whose documentation states plainly that private
applications cannot compose. That wall is exactly where the script
begins.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where-this-is-going&quot;&gt;Where this is going&lt;&#x2F;h2&gt;
&lt;p&gt;The regime where all the assumptions fail together — mutual
distrust, mutual opacity, participants unknown in advance — is not
exotic. It is regulated custody, cross-institution settlement,
confidential governance rules, private applications that need each
other’s verdicts. The field does not lack the problem; it lacks it
&lt;em&gt;visibly&lt;&#x2F;em&gt;, because the dominant architecture dissolves it at the cost
of an all-seeing middleman, and everyone has stopped seeing the cost.&lt;&#x2F;p&gt;
&lt;p&gt;That is the diagnosis, and also the apology I owe my past
conversation partners: I was describing a solution to a problem my
listeners’ architecture had defined away.&lt;&#x2F;p&gt;
&lt;p&gt;I am resurrecting these ideas from &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;0xmozak&#x2F;mozak-vm&quot;&gt;the dormant
codebase&lt;&#x2F;a&gt;, which is frozen, so
the write-ups are landing here instead. The posts that follow will cover the
mechanism in earnest: how settlement binds proofs to seats without
reading anyone’s output, why “who are you in this transaction” is a
subtler question than it looks, and how we made global state advance
in parallel by making most operations commute. The prototype was
never finished. The ideas, I think, deserve to be.&lt;&#x2F;p&gt;
&lt;p&gt;That last sentence is also an invitation. Finishing them no longer
means building a zkVM: the 2026 provers supply execution proofs and
recursion off the shelf, so what remains is the part that was always
the point — the coordination layer, the state model, and a handful of
open problems the next posts will state precisely. If picking up a
good unfinished idea appeals to you, &lt;a href=&quot;mailto:matthias.goergens@gmail.com&quot;&gt;write to
me&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>The two-time pad wanted a 5-gram, not a neural net</title>
        <published>2026-07-16T00:00:00+00:00</published>
        <updated>2026-07-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://paquari.com/posts/two-time-pad/"/>
        <id>https://paquari.com/posts/two-time-pad/</id>
        
        <content type="html" xml:base="https://paquari.com/posts/two-time-pad/">&lt;p&gt;&lt;em&gt;The original puzzle, preserved on the Internet Archive: &lt;a href=&quot;https:&#x2F;&#x2F;web.archive.org&#x2F;web&#x2F;20070303124822&#x2F;http:&#x2F;&#x2F;www.itasoftware.com&#x2F;careers&#x2F;puzzle_archive.html?catid=39&quot;&gt;Decrypting the Two-Time Pad&lt;&#x2F;a&gt;.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;There is an old ITA Software hiring puzzle from around 2004: you intercept two
messages, both encrypted with a one-time pad over a 46-character alphabet
(space, &lt;code&gt;A–Z&lt;&#x2F;code&gt;, &lt;code&gt;0–9&lt;&#x2F;code&gt;, and ten punctuation marks). Encryption is character-wise
addition modulo 46. A one-time pad is the one cipher that is provably
unbreakable — given a single ciphertext, every plaintext of the right length is
exactly as likely as any other, and no amount of computation helps. The catch,
and the whole puzzle, is in the word &lt;em&gt;one&lt;&#x2F;em&gt;. The sender used the &lt;strong&gt;same&lt;&#x2F;strong&gt; pad for
both messages.&lt;&#x2F;p&gt;
&lt;p&gt;That single reuse is fatal, and the reason is a one-line observation. Write the
ciphertexts as &lt;code&gt;C1 = P1 + K&lt;&#x2F;code&gt; and &lt;code&gt;C2 = P2 + K&lt;&#x2F;code&gt;, all mod 46. Subtract them, and
the pad — the same random &lt;code&gt;K&lt;&#x2F;code&gt; in both — cancels:&lt;&#x2F;p&gt;
&lt;pre style=&quot;background-color:#ffffff;color:#323232;&quot;&gt;&lt;code&gt;&lt;span&gt;C1 − C2 = (P1 + K) − (P2 + K) = P1 − P2   (mod 46)
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The randomness is gone. What is left is the character-wise difference of the two
&lt;em&gt;plaintexts&lt;&#x2F;em&gt;, known exactly, at every position. The cryptography has evaporated
and left behind a language puzzle: given &lt;code&gt;P1 − P2&lt;&#x2F;code&gt;, find two English texts that
have that difference.&lt;&#x2F;p&gt;
&lt;p&gt;I understood this part on the first afternoon. Everything I did &lt;em&gt;after&lt;&#x2F;em&gt; it is the
story, and most of it was a mistake.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-descent&quot;&gt;The descent&lt;&#x2F;h2&gt;
&lt;p&gt;The obvious thing, if you have spent any time around machine learning, is to
learn the map directly. Generate endless training pairs from known books, feed a
network the differences, train it to emit the two plaintexts. It barely learned.
For a long time I assumed I was holding it wrong — wrong architecture, wrong
loss, not enough data — when in fact the framing was doomed. The map from a
window of differences to a plaintext character is savagely multimodal: many
locally-plausible splittings agree with the same differences, and which one is
right depends on context that can sit dozens of characters away. Train a network
to regress on that with hard labels and it dutifully averages all the valid
answers into mush. Nothing local and feed-forward can do this, because the
disambiguating evidence is not local.&lt;&#x2F;p&gt;
&lt;p&gt;So I concluded I needed a &lt;em&gt;real&lt;&#x2F;em&gt; language model, and went to build one. This is
where the git history gets embarrassing. I did an architecture search: dense
residual networks with PReLU, GRUs, a plain RNN, DenseNet-style skip connections,
before settling on a two-layer LSTM — BatchNorm before the recurrence, mixed
precision, a 150-character window, gradient clipping, learning-rate schedules. I
preprocessed 17 GB of Project Gutenberg into byte indices with a Rust tool that
memory-mapped the corpus and streamed random snippets to the GPU over a pipe, so
I would never run out of training data. There are branches in that repository
called &lt;code&gt;fractal&lt;&#x2F;code&gt;, &lt;code&gt;frac-again&lt;&#x2F;code&gt;, &lt;code&gt;truncated&lt;&#x2F;code&gt;, and &lt;code&gt;more-loss&lt;&#x2F;code&gt;. There is a whole
line of work on recovering language statistics from the differences &lt;em&gt;alone&lt;&#x2F;em&gt;,
never looking at a plaintext — minimum-entropy objectives, iterative proportional
fitting, and eventually an FFT-based gradient-descent scheme for trigrams that I
was very proud of for getting a “77.6% improvement” on a number that should never
have been on the critical path.&lt;&#x2F;p&gt;
&lt;p&gt;All of it was in service of a prior. And the prior was not the hard part.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-part-that-mattered&quot;&gt;The part that mattered&lt;&#x2F;h2&gt;
&lt;p&gt;The thing that actually broke the cipher is a beam search over the two texts at
once. Walk left to right. At each position, for each surviving hypothesis, try
all 46 possible characters for &lt;code&gt;P1&lt;&#x2F;code&gt;; the corresponding &lt;code&gt;P2&lt;&#x2F;code&gt; character is &lt;em&gt;forced&lt;&#x2F;em&gt;,
because their difference is known. Score how English-like both halves are under
your language model, keep the best few thousand hypotheses, prune the rest. At
the end, read off the lowest-loss pair.&lt;&#x2F;p&gt;
&lt;p&gt;Once that was in place, it worked — and the model barely mattered. The LSTM was
enormous overkill for the job it was doing. Which raises the obvious question I
had spent two years not asking: how well does the &lt;em&gt;dumbest possible&lt;&#x2F;em&gt; prior do?&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-baseline-i-skipped&quot;&gt;The baseline I skipped&lt;&#x2F;h2&gt;
&lt;p&gt;So I rebuilt the whole thing from scratch around the baseline I should have
started with. The language model is a character 5-gram: for every four-character
context, count what character came next, across a couple of dozen public-domain
books. Unseen contexts back off to shorter ones with Witten-Bell smoothing, so
nothing ever gets zero probability. That is the entire model. It has no
parameters to train, no GPU, no schedule. Built from 24 million characters, it
takes about seven seconds.&lt;&#x2F;p&gt;
&lt;p&gt;Drop it into the same beam search, and it recovers about &lt;strong&gt;97% of the characters&lt;&#x2F;strong&gt;
of ordinary prose. The errors are almost all proper nouns and digit runs — the
places where both texts are genuinely unpredictable at the same spot, and the
information to separate them simply is not there. And here is the part that
stung: widening the beam past about four thousand hypotheses changed the answer
by nothing at all. The search was already sitting at the model’s ceiling. Every
gigabyte of corpus and every GPU-hour of LSTM had been buying me almost nothing
that seven seconds of counting did not already provide.&lt;&#x2F;p&gt;
&lt;p&gt;Pointed at the puzzle’s actual ciphertexts, it resolves them into the opening of
Thomas Hardy’s &lt;em&gt;The Mayor of Casterbridge&lt;&#x2F;em&gt; — the hay-trusser walking to
Weydon-Priors with his wife and Elizabeth-Jane — and a chapter of Darwin’s
&lt;em&gt;Voyage of the Beagle&lt;&#x2F;em&gt;, the one about the volcanic craters and giant tortoises of
the Galápagos. Neither book is in the training corpus. You can paste the
recovered text into a search engine and name them both.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-lesson-i-would-actually-keep&quot;&gt;The lesson I would actually keep&lt;&#x2F;h2&gt;
&lt;p&gt;It would be cheap, and wrong, to conclude that n-grams beat neural networks. They
do not, in general. The real lesson is about what a baseline &lt;em&gt;buys&lt;&#x2F;em&gt; you, and it is
not just that it is cheaper.&lt;&#x2F;p&gt;
&lt;p&gt;This problem has two independent hard parts: is the search right, and is the
prior good enough? Jumping straight to the LSTM entangled them. When nothing
worked, I could not tell whether the beam was broken, or the model undertrained,
or the optimiser merely sulking — three sources of failure firing at once, which
is exactly the fog that turns into years of rabbit holes. A 5-gram nails “good
enough prior” in seven seconds and lets you test the search &lt;em&gt;in isolation&lt;&#x2F;em&gt;. And
the search was the entire discovery. I had it early, and then buried it under a
model for two years.&lt;&#x2F;p&gt;
&lt;p&gt;The reframe that makes this obvious in hindsight is that the language model —
neural or counted — is only ever a &lt;em&gt;smoother&lt;&#x2F;em&gt;: an estimate of &lt;code&gt;P(next char | context)&lt;&#x2F;code&gt;. The beam search is identical whatever you plug in. My LSTM was an
extravagantly expensive way to answer a question a lookup table answers well
enough. And the deepest reason the puzzle is solvable at all is the same fact
from the other side: a one-time pad is safe only because the pad looks like pure
noise and demands the message look equally like noise. English is the opposite of
noise. Reusing the pad leaks exactly the redundancy that lets you pull two
overlaid texts back apart — and that redundancy is so thick that counting
letters, and refusing to commit too early, is enough.&lt;&#x2F;p&gt;
</content>
        
    </entry>
</feed>
