<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Aretta Blog</title>
    <link>https://aretta.ai/blog</link>
    <atom:link href="https://aretta.ai/feed.xml" rel="self" type="application/rss+xml"/>
    <description>Writing on software verification, formal methods, and the Aristo SDK from the Aretta team.</description>
    <language>en</language>
    <item>
      <title>Curiosity-guided bug discovery</title>
      <link>https://aretta.ai/blog/ask-campaign-bug-loop/</link>
      <guid isPermaLink="true">https://aretta.ai/blog/ask-campaign-bug-loop/</guid>
      <pubDate>Sun, 30 Aug 2026 12:00:00 GMT</pubDate>
      <description>The bugs good testing misses are the ones nobody thought to check for. A developer's hunch about a weak spot is a sharp pointer to one of those gaps, if acting on it is cheap. Our loop makes it cheap: a plain question becomes a scoped, machine-checked campaign the customer approves, and the campaign either proves the guarantee or finds the bug. Here is one question about short disk reads during recovery, and the silent data-loss bug it turned up, filed as turso#8593.</description>
      <content:encoded><![CDATA[<p>Applying verification in a scalable and automated manner has always faced a division of expertise. While developers are best placed to identify most critical parts of their codebase and identify potentially dangerous scenarios, applying formal verification itself has been challenging. You had to write a formal specification, set up a proof harness, perhaps learn a proof language. On the other hand, the formal verification expert often operates on the other end: they are masters of the core technique but pointing at the most critical system component has required closely working with the engineering team, something that does not scale. Our key insight is connecting the developer's intuition for weak spots with the verification expert's technical prowess.</p>
<h2 id="follow-the-curiosity">Follow the curiosity<a class="h-anchor" href="#follow-the-curiosity" aria-label="Link to this section">#</a></h2>
<p>Aretta's <em>curiosity-guided bug discovery loop</em> enables this connection between the developer who knows where to look and the expert who knows how to verify. The loop has three surfaces: Ask, Campaign, and Finding. In <strong>Ask</strong>, a developer poses questions about their verification record in plain English: which parts have been modelled, and the set of guarantees checked so far. When the record has no answer, because no guarantee on file covers the question, the question becomes a <strong>campaign</strong>: a newly scoped piece of modelling and verification work, priced up front and tied to a concrete outcome, a new guarantee added to the record and a verdict on whether the code upholds it. If the campaign finds that a guarantee fails, it produces a <strong>finding</strong>: a bug, described in plain language, on the developer's dashboard.</p>
<p>The customer sets the direction. They decide which question is worth a campaign, they see the price before approving it. We verify where developers point us, rather than based on our guesswork.</p>
<h2 id="a-question-about-short-reads">A question about short reads<a class="h-anchor" href="#a-question-about-short-reads" aria-label="Link to this section">#</a></h2>
<p>For Turso, one of our design partners building a modern rewrite of SQLite in Rust, we can put the following question to the Ask surface:</p>
<blockquote>
<p>If a disk read comes up short mid-recovery, could it silently lose data I have already committed?</p>
</blockquote>
<p>A disk read can return fewer bytes than requested; correct code checks for this and reads the rest. The danger is a short read during recovery, when the engine reads its write-ahead log to restore committed data after a crash. Mishandled there, committed data can be dropped, with no error raised.</p>
<p>Ask draws its answers from the verification record, the models and proofs we have already built, but sometimes the record has none. Here it covered durability once data is checkpointed, but nothing on it addressed a short read during recovery, before the data is folded into the main database.</p>
<p>We close a gap like this by expanding the modelling and verification, and here that meant a campaign: <em>Verify Turso surfaces I/O errors instead of silently swallowing them</em>, seeded by a lead from the <a href="/blog/how-aristo-found-a-durability-bug/">durability work we wrote about earlier</a>.</p>
<figure><img src="/blog/loop-wal-ask.png" alt="Reconstruction of the Ask surface with a question and a proposed campaign card."><figcaption><span class="fig-label">Fig. 1</span>Illustrative reconstruction. Ask answers from the record today; proposing a campaign from the chat is a capability we are building. The campaign card text, price, and estimate are verbatim from the live system; the question and reply are composed for this illustration.</figcaption></figure>
<h2 id="what-the-check-found">What the check found<a class="h-anchor" href="#what-the-check-found" aria-label="Link to this section">#</a></h2>
<p>Once approved, the campaign wrote a machine-checked specification of one guarantee: a failed or short disk read on a durability-critical path must be surfaced, never quietly treated as success. Further it attempted to verify the guarantee against the live engine. The check failed. When injecting a short read at the moment recovery reads the log, the above guarantee was violated. On the other hand, when running the identical fault against SQLite as a control, the guarantee held.</p>
<p>The failure of this guarantee is a consequential <strong>data-loss class bug</strong>. The database engine keeps recently committed rows in a write-ahead log (WAL). This is a side file it replays at open to restore anything not yet folded into the main database. The write-head log begins with a small header. When the read of that header is fragmented, by even a few bytes, the whole log is read as empty, and every committed row in it is discarded. Moreover, this data loss is silent as the query returns, the process exits <code>0</code>, and <code>PRAGMA integrity_check</code>, the built-in health check, reports <code>ok</code>.</p>
<figure><img src="/blog/loop-wal-repro.png" alt="Terminal reproduction: a faulted reopen returns 0 rows and ok; a clean reopen returns 40 rows."><figcaption><span class="fig-label">Fig. 2</span>The reproduction: two reopens of the same committed bytes. With a short read of the log header injected, 40 committed rows read back as 0 and the integrity check still reports ok; with no fault, all 40 return.</figcaption></figure>
<h2 id="what-it-means">What it means<a class="h-anchor" href="#what-it-means" aria-label="Link to this section">#</a></h2>
<p>We reported this data loss bug on the project's public tracker, <a href="https://github.com/tursodatabase/turso/issues/8593">turso#8593</a>, with a reproduction. The launched campaign is now associated with a result: <em>1 bug found</em>. Further the test capturing the bug durably persists in CI/CD and re-runs on every later change. Thus, the bug cannot be re-introduced unnoticed.</p>
<figure><img src="/blog/loop-wal-finding.png" alt="The finding as the customer saw it, with the link to the upstream issue."><figcaption><span class="fig-label">Fig. 3</span>The finding as the customer saw it: the guarantee in plain language, the history of the report, the link to turso#8593, and what the check established: 40 committed rows to 0 under a short read, all 40 back on a clean reopen, all 40 kept by SQLite under the same fault.</figcaption></figure>
<p>The specification behind this bug was not represented in any tests. It surfaced because of a curiousity about one specific scenario of disk behaviour. Asking a plain natural language question rather than a week of harness-building resulted in the bug discovery. This is the whole idea: the most valuable checks are aimed where a developer already suspects a weak spot, so the job is to make aiming them cheap. We add to the verification record one question at a time, guided by where developers think the risk lies.</p>]]></content:encoded>
    </item>
    <item>
      <title>Bringing Aristo to networking infrastructure correctness</title>
      <link>https://aretta.ai/blog/bringing-aristo-to-networking/</link>
      <guid isPermaLink="true">https://aretta.ai/blog/bringing-aristo-to-networking/</guid>
      <pubDate>Fri, 21 Aug 2026 12:00:00 GMT</pubDate>
      <description>We are expanding Aristo, our verification SDK, to networking. As a first demonstration we aimed it at HTTP libraries and found a correctness bug in hyper: a request accepted when it arrives in one piece and rejected when the same bytes arrive in two. Reported upstream, and fixed within three days.</description>
      <content:encoded><![CDATA[<p>We are expanding <a href="https://github.com/aretta-ai/aristo">Aristo</a>, our verification SDK, to networking. As a first demonstration we aimed it at HTTP libraries. Amongst other results, we found a correctness bug in hyper, a popular Rust HTTP library: a request whose headers end in an uncommon but supported way is accepted when it arrives in one piece, and rejected as incomplete when the same bytes arrive in two. We <a href="https://github.com/hyperium/hyper/issues/4145">reported it upstream</a>, and it has since been <a href="https://github.com/hyperium/hyper/pull/4147">fixed</a>.</p>
<h2 id="why-networking">Why networking<a class="h-anchor" href="#why-networking" aria-label="Link to this section">#</a></h2>
<p>Network protocol code is critical for two key reasons. Firstly, it provides load-bearing guarantees and abstractions that the software infrastructure built above it depends on. Secondly, it is subject to untrusted inputs that come from the outside world. Further, these inputs are highly unpredictable, since how bytes arrive depends on the network path, the current system load, and other conditions nobody controls. In a handwritten test, the input arrives in one write; on a real network, the same bytes can arrive in many pieces. That gap holds a whole class of behavior that example-based test suites cannot reach.</p>
<p>Mistakes here are not always benign either. Protocol code sits at a trust boundary, and when an implementation interprets input differently from the components around it, the disagreement itself becomes attack surface: entire attack classes, <a href="https://portswigger.net/research/http-desync-attacks-request-smuggling-reborn">request smuggling</a> among them, are built on exactly such disagreements, and <a href="https://arxiv.org/abs/2405.17737">differential studies</a> of HTTP implementations keep surfacing new ones. Making protocol code interpret input correctly removes the disagreements these attacks depend on, so verifying its correctness is also a security measure.</p>
<h2 id="underlying-method">Underlying method<a class="h-anchor" href="#underlying-method" aria-label="Link to this section">#</a></h2>
<p>Aristo achieves network verification with the same interface described in <a href="/blog/why-verified-software-remains-elusive/">our blogpost on the verification loop</a>: an <em>intent</em> (a guarantee written as a plain claim), a model that makes the claim precise, and checks that hold the code to the claim on every input it can reach, with the ability to re-run verification as the code changes.</p>
<p>Consider HTTP framing as an example. The intent is a single sentence:</p>
<blockquote>
<p>The messages parsed from a connection depend on the bytes alone, never on how those bytes were split across reads.</p>
</blockquote>
<p>Our technique developed a formal model of HTTP/1.1 framing and proved that the model satisfies this claim. We then tested hyper against the same claim, replaying byte streams under every possible split and checking whether the outcomes matched.</p>
<h2 id="the-first-result">The first result<a class="h-anchor" href="#the-first-result" aria-label="Link to this section">#</a></h2>
<p>The outcomes matched on nearly every input. They differed on one: a request with an uncommon but accepted header ending. The result is a refusal in the form of a dropped connection. The cause is a fast-path scan that runs only when a request arrives split: it catches some endings of a header block, but not the one in question. We <a href="https://github.com/hyperium/hyper/issues/4145">reported it upstream</a> with this diagnosis, and a community <a href="https://github.com/hyperium/hyper/pull/4147">fix</a> was merged that extends the fast-path scan to cover the missing ending.</p>
<figure><img src="/blog/split-delivery.svg" alt="The same bytes: delivered whole, the request is accepted; delivered split in two pieces, the connection is rejected."><figcaption><span class="fig-label">Fig. 1</span>The same bytes, two outcomes: accepted when delivered whole, rejected when split in two.</figcaption></figure>
<p>A model-based test systematically asks questions of the code that ad-hoc reviews and example-based tests alone cannot. For hyper, it took a couple of hours to catch a bug. The bug we found is a correctness issue, but as the smuggling discussion above suggests, protocol code carries security guarantees as well. Aristo checks both the same way: stated precisely, checked under every scenario, and re-checked as the code changes.</p>
<h2 id="work-with-us">Work with us<a class="h-anchor" href="#work-with-us" aria-label="Link to this section">#</a></h2>
<p><a href="https://github.com/aretta-ai/aristo">Aristo</a> is free and open source. The full service runs this method at depth and continuously as the code changes. If you build proxies, gateways, protocol libraries, or anything that parses bytes off a wire, <a href="https://forms.gle/CVzU1SX7GQdakgY27">apply to join</a>, or reach us at <a href="mailto:hello@aretta.ai">hello@aretta.ai</a>.</p>]]></content:encoded>
    </item>
    <item>
      <title>Testing samples time: SQLite's WAL reset bug and the case for proofs</title>
      <link>https://aretta.ai/blog/sqlite-wal-reset-proofs/</link>
      <guid isPermaLink="true">https://aretta.ai/blog/sqlite-wal-reset-proofs/</guid>
      <pubDate>Mon, 17 Aug 2026 12:00:00 GMT</pubDate>
      <description>SQLite is tested better than almost any software on Earth, yet it carried the WAL reset data race for sixteen years, because the race is rare in time and testing samples time. Reasoning from the published WAL contract, we located the exact window and turned it into a deterministic test on the real binary: 3.51.2 loses committed writes, 3.51.3 keeps them. The case for formal methods as a source of speed, not a tax on it.</description>
      <content:encoded><![CDATA[<p>SQLite is tested to a standard most software never approaches. Its test suite is hundreds of times the size of the library itself, it reaches the 100% modified condition/decision coverage that avionics software is held to, and it runs under continuous fuzzing. If thorough testing were ever enough on its own, SQLite is where it would be.</p>
<p>And yet a data race lived in its write-ahead log for sixteen years. It corrupted a production database at Tailscale nineteen times over six months before anyone understood why. It was so rare that the SQLite developers, once they had found it, had to add code to their own test harness just to provoke it.</p>
<p>Proofs have a reputation as the slow option, the heavyweight tool you reach for in avionics or cryptography when you have time to spare. This is a case for the opposite view, argued on the least sympathetic example we could find. We checked one property, taken straight from SQLite's own WAL specification, by reasoning rather than by running the code, and that was enough to locate the exact window this bug lives in, without the race ever having to happen.</p>
<h2 id="the-bug-briefly">The bug, briefly<a class="h-anchor" href="#the-bug-briefly" aria-label="Link to this section">#</a></h2>
<p>SQLite records recent writes in a write-ahead log and later folds them into the main database file, a step called a checkpoint; once a log is fully checkpointed, a writer may reset it and start again at the beginning. The bug is a rare data race between a checkpoint and a concurrent reset: the checkpoint misses the reset, keeps working from a stale view of the log, and writes stale log data back over the database file. Committed writes disappear, and the file can fail <code>PRAGMA integrity_check</code>. SQLite documents the bug in <a href="https://www.sqlite.org/wal.html#walresetbug">its own WAL spec</a>; <a href="https://tailscale.com/blog/sqlite-wal-reset-bug">Tailscale</a>, who hit it repeatedly in production, spent months tracing it with the SQLite developers and wrote that story up; Carl Sverre, writing at <a href="https://antithesis.com/blog/2026/wal-reset-bug/">Antithesis</a>, reproduced it by searching schedules. SQLite fixed it in version 3.51.3. Ours is a fourth account, by a different method.</p>
<h2 id="why-testing-can-miss-a-bug-like-this">Why testing can miss a bug like this<a class="h-anchor" href="#why-testing-can-miss-a-bug-like-this" aria-label="Link to this section">#</a></h2>
<p>The bug survived sixteen years of excellent testing for one reason: it is rare in time, and testing samples time. To see it, the exact interleaving has to occur: one connection resets the log in the same narrow instant another is midway through a checkpoint. You can run that workload for months and never land on it.</p>
<p>It is not rare in place, though. The interleaving corresponds to a fixed, small region of the code, and that is the opening: enumerate the schedules a piece of code admits, instead of sampling them by running it, and you never need the rare moment to arrive.</p>
<h2 id="the-property-taken-from-the-spec">The property, taken from the spec<a class="h-anchor" href="#the-property-taken-from-the-spec" aria-label="Link to this section">#</a></h2>
<p>One sentence in, a runnable bug out. The developer writes the intent; the Aretta backend does the rest, automatically:</p>
<figure><img src="/blog/wal-pipeline.svg" alt="The developer writes the intent &#x22;a checkpoint never makes committed data invisible&#x22;; the Aretta backend models the WAL checkpoint and reset protocol and checks that every interleaving preserves durInv; the check fails and produces a counterexample in which a checkpoint reads a stale WAL header; the counterexample&#x27;s schedule is transplanted onto SQLite and run, where 3.51.2 loses committed rows while 3.51.3 is clean."><figcaption><span class="fig-label">Fig. 1</span>From one sentence of intent to a machine-checked counterexample to a deterministic test: stock 3.51.2 fails it, stock 3.51.3 passes.</figcaption></figure>
<p><a href="https://github.com/aretta-ai/aristo">Aristo</a>, the verification tool we build, starts from an <em>intent</em>: a property stated in plain language, the way you would put it in a design doc. The <a href="/blog/how-aristo-found-a-durability-bug">durability bug we found in Turso</a> started from an intent too. Here, the intent is a standard durability claim, and it is the claim SQLite's own WAL documentation already makes:</p>
<blockquote>
<p>After a checkpoint, the database file reflects every committed frame up to the checkpoint point. A checkpoint never makes committed data invisible.</p>
</blockquote>
<p>That sentence, an Aristo intent lifted straight from the WAL spec, is the whole input. Everything after it runs on Aretta's verification service. The service maps the intent to a formal statement and to a <em>formal</em> model of the checkpoint and reset protocol, then checks that model against the property, and that check is what surfaced the bug. Stated over the value a reader returns for a page, the property becomes the model's durability invariant:</p>
<div class="codewrap" data-lang="lean"><pre><code class="hljs language-lean"><span class="hljs-comment">-- durability: an actor with an open read transaction reads, for every page,</span>
<span class="hljs-comment">-- exactly the committed value as of when its transaction began. A lost</span>
<span class="hljs-comment">-- committed write, or a value resurrected from an overwritten log frame,</span>
<span class="hljs-comment">-- makes this false.</span>
<span class="hljs-keyword">def</span> durInv (c : Config) : <span class="hljs-type">Bool</span> :=
  c.th.all (<span class="hljs-keyword">fun</span> t => readerDurable c.shm t.loc) &#x26;&#x26; freshDurable c
</code></pre></div>
<p>The check asks one question: does every interleaving of the checkpoint and reset protocol preserve <code>durInv</code>? The objection to preempt here is that we knew the answer and steered toward it, so it is worth being exact: the input was that one sentence. We did not write the formal statement, the model, or the interleavings, and nothing we gave the service named a bug, a place to look, a symptom, or a fix. We read the fix only afterward, to confirm the window matched. We brought exactly one piece of prior knowledge: we pointed the service at the WAL and checkpoint code rather than at all of SQLite, and durability is where any verification of a write-ahead log begins. Neither choice says anything about this defect. Everything else came from the infrastructure.</p>
<p>The answer is no, and the failing case is concrete: a fifty-three-step interleaving of a writer, a passive checkpointer, and a reader ends in a state where <code>durInv</code> is <code>false</code>. That is a theorem, checked by the Lean kernel:</p>
<div class="codewrap" data-lang="lean"><pre><code class="hljs language-lean"><span class="hljs-keyword">theorem</span> ce1_violates_durability :
    (run initReset schedCE1).map durInv = some <span class="hljs-literal">false</span> := <span class="hljs-keyword">by</span> <span class="hljs-built_in">decide</span>
</code></pre></div>
<p>The counterexample is small enough to print. A schedule in the model is nothing but a list of actor ids, one per sub-operation, and <code>schedCE1</code> is fifty-three of them. Laid out on a timeline, the race is visible to the eye: the checkpointer caches its view of the log just before the reset lands, and everything that follows executes faithfully against that stale snapshot.</p>
<figure class="fig-xl"><img src="/blog/wal-schedule.svg" alt="The fifty-three steps of schedCE1 on three lanes: writer, passive checkpointer, and reader. The checkpointer caches the header just before the writer&#x27;s reset lands, later publishes nBackfill computed from the stale header, and the reader then reads the old value of page 0."><figcaption><span class="fig-label">Fig. 2</span>schedCE1 laid out on a timeline: each tick is one sub-operation of the actor in its lane. The five marked moments carry the whole bug, each pinned to the wal.c line it models.</figcaption></figure>
<p>The counterexample also violates an invariant SQLite's own source comments assert, that "the nBackfill number is never greater than WalIndexHdr.mxFrame" (<code>src/wal.c:342</code>), evidence that the model tracks the real protocol rather than a caricature of it. And the fix guards exactly this window: version 3.51.3 adds a check that, after the checkpoint takes its read lock, detects that the log was reset out from under it and declines to write the stale data back.</p>
<h2 id="from-the-formal-model-to-the-binary">From the <em>formal</em> model to the binary<a class="h-anchor" href="#from-the-formal-model-to-the-binary" aria-label="Link to this section">#</a></h2>
<p>A counterexample in a <em>formal</em> model is a mathematical refutation: the Lean kernel certifies that the modeled protocol can reach a state where <code>durInv</code> is <code>false</code>. What it refutes, though, is the model, and a model can misdescribe the code it stands for. Aretta's backend discharges that doubt automatically: it replays the counterexample's schedule against the real library, holding a checkpoint inside the window while a writer resets the log. What reaches the developer is a runnable reproduction on a stock build, not a claim about a model. Under that schedule, a stock build of 3.51.2 loses committed rows, and in most runs the file is corrupted as well. A stock build of 3.51.3, under the identical schedule, does neither.</p>
<table>
<thead>
<tr>
<th>SQLite build</th>
<th>same forced schedule</th>
</tr>
</thead>
<tbody>
<tr>
<td>3.51.2 (<code>ba76c160</code>)</td>
<td>committed rows lost; <code>PRAGMA integrity_check</code> fails</td>
</tr>
<tr>
<td>3.51.3 (<code>a5333afb</code>)</td>
<td>every committed row present; check passes</td>
</tr>
</tbody>
</table>
<p>The stall the schedule needs is not exotic. A pause of a few milliseconds at the right point, the kind an ordinary scheduler hands out for a page fault or a slow syscall, is enough. That is how the bug reached production.</p>
<h2 id="search-and-reasoning">Search and reasoning<a class="h-anchor" href="#search-and-reasoning" aria-label="Link to this section">#</a></h2>
<p>Antithesis reproduced this bug by searching schedules; we located it by reasoning about them. The two routes are complementary. Search needs no <em>formal</em> model and is the right tool when you cannot build one: it explores executions until one trips an assertion. Reasoning needs a model, and in exchange it hands you the mechanism itself: the violated property, a machine-checked counterexample, and the exact window in the code, all before the code runs once. To be clear about credit, we did not find this bug; SQLite and Tailscale did. We showed that one property, taken from the spec, is enough to pin it down.</p>
<h2 id="the-mindset-this-should-change">The mindset this should change<a class="h-anchor" href="#the-mindset-this-should-change" aria-label="Link to this section">#</a></h2>
<p>The slow path to this bug was real: months of production corruption, forensic telemetry in a live system, and in the end, test-harness code written just to force the bug. The reasoning path was a property and a <em>formal</em> model, and it skipped all of that: the incident, the irreproducible flake, the triage.</p>
<p>Formal methods have long been filed under caution, a luxury for teams with time and budget to spare. <a href="/blog/why-verified-software-remains-elusive">What made verification elusive</a> was its cost, and that cost is falling. As it falls, the arithmetic turns over: reasoning about where a bug must be is cheaper than waiting for it, reproducing it, and triaging it after an outage. The deterministic test the reasoning leaves behind is a <a href="/blog/proof-maintenance-with-aristo">standing guard that re-runs on every change</a>. On software tested as thoroughly as any on Earth, a failure that took months to run down in production fell out of a single sentence of its own spec. Testing samples time. A proof does not have to.</p>
<hr>
<p><em>A closing indulgence: the property above is not new. It is the WAL specification, which SQLite has published all along. So, at the risk of hindsight bravado, the bug was checkable the day it was written in 2010, and finding it never needed the race to happen even once. Aristo did not exist in 2010. Alas.</em></p>]]></content:encoded>
    </item>
    <item>
      <title>The proof maintenance problem</title>
      <link>https://aretta.ai/blog/proof-maintenance-with-aristo/</link>
      <guid isPermaLink="true">https://aretta.ai/blog/proof-maintenance-with-aristo/</guid>
      <pubDate>Fri, 07 Aug 2026 12:00:00 GMT</pubDate>
      <description>Turso merged a fix for a crash-safety bug we had reported, and the issue closed automatically. Re-checking the whole model at the fix commit showed the fix was correct but had broken a neighboring durability specification no regression test covered. Keeping the whole proof current as the code changes is the problem Aretta solves.</description>
      <content:encoded><![CDATA[<p>Earlier this week, Turso, the team building a modern rewrite of SQLite in Rust and one of our design partners, merged <a href="https://github.com/tursodatabase/turso/pull/8101">a PR</a> that closed <a href="https://github.com/tursodatabase/turso/issues/7952">a bug issue</a> that Aretta had filed as part of <a href="/blog/how-aristo-found-a-durability-bug/">verification work we have written about before</a>. However, the aretta-ci bot reopened the issue, flagging it as "do not close". <em>There was no human involvement, either on Turso's or our side.</em> Why did this happen?</p>
<h2 id="the-gap-that-regression-tests-cannot-fill">The gap that regression tests cannot fill<a class="h-anchor" href="#the-gap-that-regression-tests-cannot-fill" aria-label="Link to this section">#</a></h2>
<p>The fix was correct for the reported bug; however, <strong>it broke a neighboring, unreported durability specification</strong>. Detecting the second break is beyond the scope of an ordinary regression test suite.</p>
<p>An ordinary regression test would have been enough for the reported bug. After this fix, Turso's suite would gain a test that reproduces the checkpoint bug, the fix would make that test pass, and the test would stay in the suite to detect the same behavior if it ever returned. The limit is coverage. A regression test covers exactly the property it was written for, and a suite built this way has tests only for bugs that have already been found. So after a fix that repairs the known bug but breaks something else, every existing test still passes: there is no test for the property that broke.</p>
<h2 id="how-aristo-caught-it">How Aristo caught it<a class="h-anchor" href="#how-aristo-caught-it" aria-label="Link to this section">#</a></h2>
<p>Aristo starts from the guarantee instead of from the bug history. In <a href="/blog/how-aristo-found-a-durability-bug/">the first post about this engagement</a> we showed how that works. The top-level promise is decomposed into a conjunction of smaller specifications, <code>S ⟺ A1 ∧ A2 ∧ …</code>, and those into smaller ones, until every leaf is something you can check against the code. A bug, in this setting, is a leaf specification found to be false.</p>
<p>Instantiated for Turso's write-ahead-log subsystem (the symbols are schematic, so these leaves differ from the first post's tree), the top-level promise <code>S</code> is that a crash never corrupts or loses committed data, and two of its leaves are:</p>
<ul>
<li><code>A1</code>: checkpointing is crash-atomic. A crash during a checkpoint never results in a torn database file. The reported bug (issue #7952) was a failure of this specification.</li>
<li><code>A2</code>: a COMMIT under <code>PRAGMA synchronous=FULL</code> does not return until its WAL frames are fsync-durable, so an acknowledged commit survives a crash. This one had passed every check so far.</li>
</ul>
<figure><img src="/blog/proof-tree.svg" alt="A proof tree: the top-level guarantee S at the root decomposes into a conjunction of many leaf specifications; A1 and A2 are two of the leaves."><figcaption><span class="fig-label">Fig. 1</span>The top-level guarantee S decomposes into leaf specifications; A1 (the reported bug) and A2 (the regression) are siblings on the same tree.</figcaption></figure>
<p>Sibling specifications like <code>A2</code> are the point: nobody wrote all the leaf specifications in anticipation of a particular failure; they exist because the proof of the top-level guarantee needs them, so the model already has coverage wherever a fix might have side effects.</p>
<h2 id="re-checking-the-model-at-the-fix-commit">Re-checking the model at the fix commit<a class="h-anchor" href="#re-checking-the-model-at-the-fix-commit" aria-label="Link to this section">#</a></h2>
<p>When the PR merged, we re-checked the whole model, not only the reproduction for the reported bug. At the fix commit, the check of <code>A1</code> passed for the first time: the reported bug was gone. But the check of <code>A2</code>, which had passed at every revision before, now failed. Hence <code>S</code> <em>still did not hold</em>. That automated result is what the bot posted when it reopened the issue. The failure of <code>A2</code> comes down to one observable requirement: under <code>synchronous=FULL</code>, COMMIT must fsync the WAL before it returns. The table below shows that requirement checked on both revisions, with and without a checkpoint immediately before the COMMIT:</p>
<table>
<thead>
<tr>
<th>Revision</th>
<th>Setup: <code>wal_checkpoint(TRUNCATE)</code> before the COMMIT?</th>
<th>Observed: COMMIT fsyncs the WAL, as <code>A2</code> requires?</th>
</tr>
</thead>
<tbody>
<tr>
<td>parent</td>
<td>yes</td>
<td>yes</td>
</tr>
<tr>
<td>parent</td>
<td>no</td>
<td>yes</td>
</tr>
<tr>
<td><strong>fix</strong></td>
<td><strong>yes</strong></td>
<td><strong>no</strong></td>
</tr>
<tr>
<td>fix</td>
<td>no</td>
<td>yes</td>
</tr>
</tbody>
</table>
<p>Under <code>PRAGMA synchronous=FULL</code>, a COMMIT that follows a WAL-resetting checkpoint does not fsync the WAL. The frames are written and COMMIT returns success, but nothing forces them to disk. Without the preceding checkpoint the fsync happens correctly, which is why this is easy to miss in review and in ordinary test suites. In fact, this regression was more severe than the original bug: a torn file is corruption a checker can detect, while this one means an acknowledged commit is silently lost, with <code>PRAGMA integrity_check</code> still reporting "ok". None of it reached production; the failure was demonstrated in a crash simulator during verification.</p>
<h2 id="resolution">Resolution<a class="h-anchor" href="#resolution" aria-label="Link to this section">#</a></h2>
<p>Subsequently, Pekka Enberg at Turso confirmed the mechanism and implemented the full fix in <a href="https://github.com/tursodatabase/turso/pull/8172">a PR</a>, including the regression test recommended in our reopening comment. Turso closed #7952 again, this time citing the fix commit. Less than a day passed between our reopening of the issue and the merged upstream fix. Enberg also <a href="https://x.com/penberg/status/2084555513220464998">described the episode on X</a>, noting that "in hindsight I should have just run the verification step on the original fix to catch this earlier."</p>
<p>Both <code>A1</code> and <code>A2</code> remain in the model, and each new revision of Turso is re-checked against them as part of the standing report we maintain. That is the proof maintenance problem in the title, and the <a href="/blog/why-verified-software-remains-elusive/">last leg of the verification loop</a> we described earlier: re-running the proof whenever the code changes. A fix is a code change like any other, which is why re-running the whole proof, and not just the reported bug's test, is what caught the regression.</p>
<h2 id="work-with-us">Work with us<a class="h-anchor" href="#work-with-us" aria-label="Link to this section">#</a></h2>
<p><a href="https://github.com/aretta-ai/aristo">Aristo</a>, the SDK our verification service is built on, is free and open source. Deeper verification as described here is what we run with design partners, and we are onboarding 2 to 3 more. If the hardest guarantees in your system are on the crash path, <a href="https://forms.gle/CVzU1SX7GQdakgY27">apply to join</a>, or reach us at <a href="mailto:hello@aretta.ai">hello@aretta.ai</a>.</p>]]></content:encoded>
    </item>
    <item>
      <title>Why verified software remains elusive</title>
      <link>https://aretta.ai/blog/why-verified-software-remains-elusive/</link>
      <guid isPermaLink="true">https://aretta.ai/blog/why-verified-software-remains-elusive/</guid>
      <pubDate>Tue, 04 Aug 2026 12:00:00 GMT</pubDate>
      <description>Machine checkers for software have grown stronger for decades, yet uptake in production codebases has stayed low. We ask ourselves: why? To answer, we view verification as a trust-building loop between software developers and the verification infrastructure. The bottleneck, it turns out, is not checker strength but the interface.</description>
      <content:encoded><![CDATA[<p>Machine checkers for software have been getting steadily stronger for decades; we now have fully verified compilers (<a href="https://compcert.org/">CompCert</a>) and even OS kernels (<a href="https://sel4.systems/">seL4</a>). Yet software verification <a href="https://link.springer.com/chapter/10.1007/978-3-030-58298-2_1">continues to have low uptake</a>. If checker strength were the bottleneck, adoption should have followed. So why hasn't it?</p>
<h2 id="the-trust-building-verification-loop">The trust-building verification loop<a class="h-anchor" href="#the-trust-building-verification-loop" aria-label="Link to this section">#</a></h2>
<p>To answer this question, verification must be seen as a <em>process loop</em> that builds trust in the software over time:</p>
<ol>
<li><strong>Specification</strong>: deciding and communicating what to prove.</li>
<li><strong>Verification</strong>: scalably performing the proof.</li>
<li><strong>Proof reporting and maintenance</strong>: exposing proof results to the user and maintaining them.</li>
</ol>
<figure><img src="/blog/verification-loop.svg" alt="The verification loop: a developer and a verification engine, with specification flowing from developer to engine, verification cycling on the engine side, and reporting flowing back to the developer"><figcaption><span class="fig-label">Fig. 1</span>The verification loop: specification flows from developer to engine, proof runs on the engine side, and reporting returns the results and keeps them current.</figcaption></figure>
<p>Most of the advances in verification to date have been concentrated on developing novel proof languages and scalable verification algorithms; these are tools that live on the verification side. The core bottleneck to adoption is <strong>interfacing these tools with developers in the wild</strong>. This interface is where the rubber hits the road.</p>
<h2 id="specification-deciding-and-communicating-what-to-prove">Specification: deciding and communicating what to prove<a class="h-anchor" href="#specification-deciding-and-communicating-what-to-prove" aria-label="Link to this section">#</a></h2>
<p>While developers usually have specifications or invariants in their heads, traditionally verification has required exporting specifications into formal languages. First, this is challenging for the majority of developers due to unfamiliarity with formal languages. Second, formal specification languages just aren't very interpretable, making formal specifications a poor way of communicating claims and intents between developer teams. This is why the spec either never gets written or quietly rots after the first refactor. Specification, in practice, is <strong>an authoring problem long before it is a math problem</strong>.</p>
<p>At Aretta we recognized this problem early and built our SDK <a href="https://github.com/aretta-ai/aristo">Aristo</a> to remove it. Aristo puts the claim in the code, in plain English, directly above the function it describes:</p>
<div class="codewrap" data-lang="rust"><pre><code class="hljs language-rust"><span class="hljs-meta">#[aristo::assume(<span class="hljs-string">"lo &#x3C;= hi"</span>)]</span>
<span class="hljs-meta">#[aristo::intent(<span class="hljs-string">"returns a value within [lo, hi] for any input"</span>, verify = <span class="hljs-string">"neural"</span>, id = <span class="hljs-string">"clamp_in_range"</span>)]</span>
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">clamp</span>(value: <span class="hljs-type">i64</span>, lo: <span class="hljs-type">i64</span>, hi: <span class="hljs-type">i64</span>) <span class="hljs-punctuation">-></span> <span class="hljs-type">i64</span> {
    value.<span class="hljs-title function_ invoke__">max</span>(lo).<span class="hljs-title function_ invoke__">min</span>(hi)
}
</code></pre></div>
<p>The assumption records the precondition the function relies on (an interval only makes sense when lo &#x3C;= hi), and the intent records what the function guarantees under that assumption. The verify field selects how rigorously the claim is checked, from a fast local AI critic (neural) up to machine-checked proof (full). Together they address both halves of the problem: authoring needs no formal language, just plain sentences any engineer can write, and the claims are visible to every teammate and every tool, in the diff, next to the code they describe.</p>
<p><em>Agent-enabled:</em> Because Aristo ships as agent skills, it works out of the box with coding agents like Claude Code. Your agent writes these annotations as it works, and you only attest to them. Attesting is simple: the skill proposes the claim, and you as the developer accept or reject it, since the final agreement on what the code should do has to involve the developer.</p>
<h2 id="verification-performing-the-proof">Verification: performing the proof<a class="h-anchor" href="#verification-performing-the-proof" aria-label="Link to this section">#</a></h2>
<p>Once a claim exists, checking it has to run against the real codebase, and the obstacle there is <strong>observability</strong>. The state a property talks about (e.g., a private counter inside a cache, or a flag inside a write-ahead log) is deliberately hidden behind the API, and rightly so. Encapsulation is good engineering, but it also means a checker cannot see the state it needs to inspect. Aristo provides observability hooks that expose exactly this state without changing the API you ship: derive-style snapshots that let a harness read private state directly. Beyond observability, Aristo also provides test-only visibility for crate-private functions and labeled fault points for exercising failure paths. All of these hooks are feature-gated, so they compile away entirely in production builds.</p>
<div class="codewrap" data-lang="rust"><pre><code class="hljs language-rust"><span class="hljs-meta">#[derive(Inspect)]</span>          <span class="hljs-comment">// feature-gated: compiles away in production builds</span>
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">struct</span> <span class="hljs-title class_">Cache</span> {
    <span class="hljs-meta">#[inspect]</span>
    evictable: <span class="hljs-type">usize</span>,       <span class="hljs-comment">// gains inspect_evictable()</span>
    <span class="hljs-comment">// ...</span>
}
</code></pre></div>
<p><strong>The proving itself runs on Aretta's backend.</strong> The same claim can be checked today by a free local critic, then escalated to tests, fuzzing, and machine-checked proof, which is the full service we run with design partners. To make it concrete, here is one guarantee we check for Turso, a production database engine, verbatim:</p>
<blockquote>
<p>The cache's materialized evictable-page counter stays equal to the true number of evictable resident pages after every cache operation.</p>
</blockquote>
<p>That counter is private state inside the cache, and the guarantee is checkable against the running engine only because the observation points exist. No fields were made public and no fork was needed.</p>
<h2 id="reporting-and-maintenance-keeping-the-proof-alive">Reporting and maintenance: keeping the proof alive<a class="h-anchor" href="#reporting-and-maintenance-keeping-the-proof-alive" aria-label="Link to this section">#</a></h2>
<p>The last leg of the loop is the one most verification efforts never build, and it comes down to two abilities: <strong>communicating what was proved</strong> in terms the developer already speaks, and <strong>re-running the proof whenever the code changes</strong>.</p>
<p>Aristo handles the first by keeping every claim's status with the code. A claim reads as a plain sentence, its verdict is stored alongside the function, and there is no formal report to decode. It handles the second by hashing each claim against the function body, so that editing verified code flips the claim to stale, triggers the checks to run again, and puts the fresh result in the same place. Proofs go stale as soon as the code changes; keeping them current is what this leg of the loop is for.</p>
<p>Turso, the team building a modern rewrite of SQLite in Rust, is one of our design partners; we <a href="/blog/how-aristo-found-a-durability-bug/">wrote earlier</a> about a durability bug we found in their engine. Their deployment shows what this leg of the loop looks like in practice. Every week, the machine-checked guarantees we maintain for them (currently 31, 17 of them on the page cache, and growing over time) are re-checked against the current revision and published as a report their team reads in plain English. When an upstream change breaks a guarantee, its row flips red the same week with a reproduction attached; when it is fixed, the row turns green and the check remains in place as a regression guard.</p>
<h2 id="where-this-leaves-us">Where this leaves us<a class="h-anchor" href="#where-this-leaves-us" aria-label="Link to this section">#</a></h2>
<p>The provers are ready, as results like seL4 and CompCert show. Adoption has been waiting on the loop's other two legs: specifications that developers can write and read, and results that reach them and stay current as the code changes. Build those two legs, and the provers we already have can finally be applied to the code that actually ships.</p>
<p>Aristo's core is <a href="https://github.com/aretta-ai/aristo">free and open source</a>. The full service, which takes claims all the way to machine-checked proof, is what we run with design partners, and we are onboarding 2 to 3 more: <a href="https://forms.gle/CVzU1SX7GQdakgY27">apply to join</a>, or write to us at <a href="mailto:hello@aretta.ai">hello@aretta.ai</a>.</p>]]></content:encoded>
    </item>
    <item>
      <title>How Aristo found a durability bug in Turso</title>
      <link>https://aretta.ai/blog/how-aristo-found-a-durability-bug/</link>
      <guid isPermaLink="true">https://aretta.ai/blog/how-aristo-found-a-durability-bug/</guid>
      <pubDate>Wed, 08 Jul 2026 12:00:00 GMT</pubDate>
      <description>Working with the team at Turso, we found a gap in durability, the one promise a database can least afford to break: a small bug with a sharp consequence, and a good illustration of what Aristo is for. Turso wrote up their own account in 'The Final Boss of Reliability'; this is ours.</description>
      <content:encoded><![CDATA[<p>Durability is the promise a database can least afford to break. When it tells you a write succeeded, that write has to survive a crash. Everything else sits on top of that one guarantee: your balances, your orders, your audit log.</p>
<p>Working with the team at Turso, we found a case where the guarantee had a gap. It's a small bug with a sharp consequence, and a good illustration of what <a href="https://github.com/aretta-ai/aristo">Aristo</a> is for. Turso have written up their own account, generously and in detail, in <a href="https://turso.tech/blog/the-final-boss-of-reliability">"The Final Boss of Reliability"</a>; this is ours.</p>
<h2 id="the-bug">The bug<a class="h-anchor" href="#the-bug" aria-label="Link to this section">#</a></h2>
<p>Turso can run as a local replica that syncs from a cloud master database. As it commits synced pages, it writes them to disk. But writing to disk isn't the same as making data durable: the operating system can hold your bytes in a cache, and only fsync forces them down to the media. So the property is simple:</p>
<blockquote>
<p>A transaction may be reported durable only after its commit frame has reached stable storage via fsync.</p>
</blockquote>
<p>On the replica-sync path, that second step was missing. The ordering the property requires, and the ordering the code actually took:</p>
<div class="codewrap"><pre><code>correct   write frame  ──▶  fsync  ──▶  report durable
bug       write frame  ───────────────▶  report durable      (fsync skipped)
</code></pre></div>
<p>So if a crash or power loss lands before that data reaches the disk, a write the database has already acknowledged can be gone on restart: silent data loss, or a corrupted copy of the synced database. No one ever hit it; it was caught first. But it was reachable.</p>
<h2 id="why-its-hard-to-catch">Why it's hard to catch<a class="h-anchor" href="#why-its-hard-to-catch" aria-label="Link to this section">#</a></h2>
<p>So why is something reachable so hard to actually catch? Because the bug lives on the failure path. The happy path is flawless: a commit completes, everyone moves on. It only bites if a crash lands in one narrow window, and ordinary testing never goes there. Tests run on healthy hardware, where the power doesn't cut out between two lines of code; you can't unit-test a bug that only exists mid-crash.</p>
<p>So you reach for stronger testing, and Turso has some of the strongest there is. Their Deterministic Simulation Testing drops the whole system into a simulated world and injects crashes and power failures on purpose, precisely to expose bugs like this. It is genuinely excellent, and unlike ordinary tests, it can reach this class of failure at all.</p>
<p>It still didn't find this one, and not because it's weak. Testing, even this kind, works by sampling: it runs the scenarios its generators happen to produce, out of a space of crash timings and interleavings far too large to enumerate. In Turso's words, the generators "were not generating the particular situation" that triggered it. The scenario was reachable; it just wasn't reached. Extensive isn't exhaustive, and that gap is where this bug lived.</p>
<h2 id="how-aristo-found-it">How Aristo found it<a class="h-anchor" href="#how-aristo-found-it" aria-label="Link to this section">#</a></h2>
<p>Aristo starts from intent. You state what a piece of code is supposed to do as a plain-language claim, written right above it:</p>
<div class="codewrap" data-lang="rust"><pre><code class="hljs language-rust"><span class="hljs-meta">#[aristo::intent(
    <span class="hljs-string">"a transaction is reported durable only after its commit frame is fsynced"</span>,
    verify = <span class="hljs-string">"full"</span>,
    id = <span class="hljs-string">"commit_is_durable"</span>,
)]</span>
<span class="hljs-keyword">fn</span> <span class="hljs-title function_">commit_dirty_pages_inner</span>(<span class="hljs-comment">/* … */</span>) { <span class="hljs-comment">/* … */</span> }
</code></pre></div>
<p>Three parts: the <strong>claim</strong> in plain English, an <strong>id</strong> so it can be tracked as the code changes, and a <strong>verify mode</strong>. The verify mode is the bridge to how the claim gets checked. <code>neural</code> is a fast, local check: an AI critic reads the code against the claim. <code>full</code> hands the claim to Aristo's backend, the engine that reasons about the states the code can reach, not just the ones a test happens to run.</p>
<p>For a durability claim like this one, <code>full</code> is what matters. The backend looked for any reachable execution in which a transaction is reported durable before its frame is fsynced, and it found one. In minutes, and deterministically: the same answer every time, not a matter of the right random seed. How the backend does that is the part we keep to ourselves; what you see is the claim going in and the exact violating case coming back.</p>
<p>That's the shape of the whole product. You declare intent next to your code, Aristo tells you whether the code can break it, and if the code later drifts from the claim, it says so, before the change ships.</p>
<h2 id="a-bug-is-a-spec-that-didnt-hold">A bug is a spec that didn't hold<a class="h-anchor" href="#a-bug-is-a-spec-that-didnt-hold" aria-label="Link to this section">#</a></h2>
<p>What we're really after is a proof: that an acknowledged write survives a crash. You can't establish that in one shot, so the proof breaks it into smaller specs, and those into smaller ones, down to leaves you can check against the code. The tree isn't a checklist written from memory; it's what proving the top spec <em>forces</em> onto the table. Every leaf is there in service of the proof above it.</p>
<div class="codewrap"><pre><code>S   ⟺   A1 ∧ A2
A1  ⟺   B1 ∧ B2 ∧ B3
A2  ⟺   C1 ∧ C2

S   an acknowledged write survives any crash
A1  ack only after the commit frame is on stable storage      (write side)
      B1  frame bytes written to the OS                 ✓
      B2  fsync after the write, before the ack         ✗   ← the bug
      B3  ack issued only after fsync returns           ✓
A2  recovery restores every persisted frame                   (recovery side)
      C1  scan the WAL from the last checkpoint         ✓
      C2  re-apply every frame up to the last commit    ✓
</code></pre></div>
<p>Notice what that buys you. Nobody had to think to test that the replica calls fsync. Proving durability required the obligation "fsync before the ack," so it appeared on the tree on its own, and it was the leaf that came back false. The bug wasn't something we went looking for; it was a spec the proof demanded, and the code didn't hold it.</p>
<p>That's all a bug is here: a spec raised in service of a larger proof, that didn't hold.</p>
<p><em>(Every split here is an exact <code>⟺</code>, so a red leaf is a real hole in <code>S</code>, not a dead end in one proof attempt.)</em></p>
<h2 id="what-this-doesnt-mean">What this doesn't mean<a class="h-anchor" href="#what-this-doesnt-mean" aria-label="Link to this section">#</a></h2>
<p>We checked one guarantee: that acknowledged writes survive a crash. We did not verify all of Turso, and we won't imply otherwise. Verification also doesn't replace testing. Tests exercise the real system on real hardware, and Turso's simulation testing will keep finding things a single guarantee can't. The two are complementary; Turso framed it the same way, and that's the honest read. What verification adds is coverage of the failure paths you can't afford to leave to chance.</p>
<h2 id="try-aristo-and-whats-next">Try Aristo, and what's next<a class="h-anchor" href="#try-aristo-and-whats-next" aria-label="Link to this section">#</a></h2>
<p>You can try <a href="https://github.com/aretta-ai/aristo">Aristo</a> today. The core, writing intents and checking them locally, is free and open source, so you can start putting claims on your own code right away. The backend proofs that caught this bug (<code>verify = "full"</code>) are what we're onboarding design partners for, and as part of our initial launch we're taking on three or four more. If you're building a database, a storage engine, a consensus layer, or anything where a wrong answer on the failure path is unacceptable, we'd like to hear from you.</p>
<p>We're glad to be working with Turso as they head toward their 1.0 release. Getting durability right is easy to overlook and expensive to get wrong, and a team that goes looking for these bugs before their users do is exactly who you want to build alongside.</p>
<p>Join the waitlist: <a href="https://forms.gle/AYoqpeXJq2YQv57S6">forms.gle/AYoqpeXJq2YQv57S6</a></p>
<p>Or reach us directly: <a href="mailto:hello@aretta.ai">hello@aretta.ai</a></p>]]></content:encoded>
    </item>
    <item>
      <title>We Asked Two AI Agents to Build an LSM Database. Only One Got Database Compaction Right.</title>
      <link>https://aretta.ai/blog/lsm-tree-proof-vs-test/</link>
      <guid isPermaLink="true">https://aretta.ai/blog/lsm-tree-proof-vs-test/</guid>
      <pubDate>Mon, 30 Mar 2026 12:00:00 GMT</pubDate>
      <description>We built two LSM databases from the same spec — one with proof guidance, one without. 45 tests found zero bugs. A Verus proof skeleton found a silent data-loss bug in database compaction that testing couldn't reach.</description>
      <content:encoded><![CDATA[<p>We gave the same specification to two AI code-generation agents and asked each to build a persistent, crash-safe LSM database in Rust. The kind of storage engine that powers LevelDB, RocksDB, and the write path of half the infrastructure you use daily.</p>
<h2 id="the-setup">The setup<a class="h-anchor" href="#the-setup" aria-label="Link to this section">#</a></h2>
<p>Both implementations target the same <a href="https://github.com/aretta-ai/lsm-experiment/blob/main/promptspec.md">spec</a>: a key-value store with <code>put</code>, <code>get</code>, <code>delete</code>, <code>scan</code>, and <code>close</code>. Writes must survive process crashes (via a write-ahead log), background compaction must never change what queries return, and deleted keys must never come back.</p>
<p>Both received the same architectural constraints: sequence numbers on every entry, a memtable keyed by (key, reverse sequence number), leveled compaction, and bloom filters with no-false-negatives guarantees. These constraints were extracted from Verus proof skeletons to ensure the proofs could apply to either implementation.</p>
<p>The difference is what happened next:</p>
<ul>
<li><strong>Claude Code</strong> went straight from the prompt to code. No intermediate specification, no proofs — just the architectural constraints and an implementation.</li>
<li><strong>Aretta</strong> first translated the high-level prompt into a low-level formal specification, then built Verus proof skeletons for that specification, and only then implemented the code. The proofs cover 13 machine-checked properties across data-structure invariants and store-level ordering guarantees. (The full list is in the appendix below.)</li>
</ul>
<h2 id="a-torture-test-suite-of-45-tests-catches-zero-bugs">A torture test suite of 45 tests catches zero bugs<a class="h-anchor" href="#a-torture-test-suite-of-45-tests-catches-zero-bugs" aria-label="Link to this section">#</a></h2>
<p>We wrote 45 tests across seven categories: latest-write-wins, tombstone correctness, scan ordering, crash recovery, compaction transparency, scale, and edge cases. The suite includes overwrite storms of 100,000 puts to the same key, mass deletion of 10,000 keys followed by a full-range scan, and round-trip checks for binary keys containing null bytes. <strong>Every test passed on both implementations. This is the part where most teams would ship.</strong></p>
<h2 id="the-bug-that-tests-cant-find">The bug that tests can't find<a class="h-anchor" href="#the-bug-that-tests-cant-find" aria-label="Link to this section">#</a></h2>
<p>LSM databases periodically run <em>compaction</em> — merging and reorganizing on-disk files (SSTables) to reclaim space and maintain read performance. Here is the database compaction code from Claude Code:</p>
<div class="codewrap" data-lang="rust"><pre><code class="hljs language-rust"><span class="hljs-comment">// Step 1: Remove old SSTables from in-memory tracking</span>
levels[level].<span class="hljs-title function_ invoke__">retain</span>(|h| !source_ids.<span class="hljs-title function_ invoke__">contains</span>(&#x26;h.meta.id));
manifest.<span class="hljs-title function_ invoke__">remove_sstables</span>(&#x26;all_removed_ids);

<span class="hljs-comment">// Step 2: Write merged output to new SSTables</span>
<span class="hljs-keyword">let</span> <span class="hljs-variable">new_handles</span> = <span class="hljs-title function_ invoke__">write_split_sstables</span>(dir, manifest, &#x26;merged)?;
</code></pre></div>
<p><em><a href="https://github.com/aretta-ai/lsm-experiment/blob/main/agent-cc/src/compaction.rs#L113-L118">See full source</a> — <code>agent-cc/src/compaction.rs</code></em></p>
<p>Do you see the bug?</p>
<p>Step 1 removes references to the old SSTables. Step 2 writes their replacement. If step 2 fails due to an I/O error, the references are already gone and there's nothing to replace them. From this point on, <code>get()</code> silently returns <code>None</code> for every key that lived in those files.</p>
<p>The data is still on disk. A restart recovers it. But the running process has no idea it's broken. It just serves wrong answers until someone notices.</p>
<h2 id="what-the-proof-found">What the proof found<a class="h-anchor" href="#what-the-proof-found" aria-label="Link to this section">#</a></h2>
<p>Aretta's proof skeleton specifies that new SSTables must be durable before the manifest is updated, and the manifest must be durable before old files are deleted. The underlying invariant is: at every reachable state during compaction, either old SSTables are present or new SSTables are present (or both). Aretta <a href="https://code.aretta.ai/shared/b6af8620-2f4f-4f9e-af6c-71a6fc1d6219">first proved this property on an abstract model</a> of the compaction phases, then used that proof to guide the implementation — ensuring the code maintains the invariant <em>by construction</em>.</p>
<p>When we tried proving this property on Claude Code's generated code, <a href="https://code.aretta.ai/shared/5d3222f1-94b4-4ca6-92d6-f550f13629ed">Verus found the property to be violated during compaction</a>.</p>
<h2 id="confirming-the-bug-fault-injection">Confirming the bug: fault injection<a class="h-anchor" href="#confirming-the-bug-fault-injection" aria-label="Link to this section">#</a></h2>
<p>To confirm this causes real harm, we built a fault injection hook that fails the N-th <code>SSTableWriter::finish()</code> call with a simulated ENOSPC error.</p>
<div class="codewrap" data-lang="rust"><pre><code class="hljs language-rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">fn</span> <span class="hljs-title function_">fail_nth_sst_finish</span>(n: <span class="hljs-type">u32</span>) {
    SST_FINISH_COUNTDOWN.<span class="hljs-title function_ invoke__">with</span>(|c| c.<span class="hljs-title function_ invoke__">set</span>(n));
}
</code></pre></div>
<p><em><a href="https://github.com/aretta-ai/lsm-experiment/blob/main/agent-cc/src/testing.rs">See full source</a> — <code>agent-cc/src/testing.rs</code></em></p>
<p>We wrote four sentinel keys into separate L0 SSTables, then triggered compaction with the fault armed. On Claude Code's implementation, all four sentinel keys vanished from the live instance — <code>get()</code> returned <code>None</code> for every one of them. A reopen recovered the data (the files were still on disk), but the running process had been silently serving wrong answers with no indication of failure.</p>
<p>On Aretta's implementation, the same fault injection had no effect. All four sentinel keys remained visible throughout. The proof-guided implementation writes output first, then calls <code>manifest.atomic_swap()</code> which only updates in-memory state after the new SSTables are confirmed on disk:</p>
<div class="codewrap" data-lang="rust"><pre><code class="hljs language-rust"><span class="hljs-comment">// Step 1: Write new SSTables (confirmed on disk)</span>
<span class="hljs-keyword">let</span> <span class="hljs-variable">new_sstables</span> = <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">write_level_sstables</span>(manifest, merge_iter, <span class="hljs-number">1</span>)?;

<span class="hljs-comment">// Step 2: Atomic manifest swap — ADDs before REMOVEs, fsync,</span>
<span class="hljs-comment">//         THEN update in-memory state</span>
manifest.<span class="hljs-title function_ invoke__">atomic_swap</span>(new_sstables, old_file_numbers)?;

<span class="hljs-comment">// Step 3: Delete old files (manifest already durable)</span>
<span class="hljs-keyword">for</span> <span class="hljs-variable">entry</span> <span class="hljs-keyword">in</span> old_entries { fs::<span class="hljs-title function_ invoke__">remove_file</span>(...)?; }
</code></pre></div>
<p><em><a href="https://github.com/aretta-ai/lsm-experiment/blob/main/agent-a/src/compaction.rs#L116-L134">See full source</a> — <code>agent-a/src/compaction.rs</code></em></p>
<p>If the write fails at step 1, <code>atomic_swap</code> is never called and the in-memory state is unchanged. The invariant holds at every phase.</p>
<h2 id="why-this-bug-is-hard-to-catch">Why this bug is hard to catch<a class="h-anchor" href="#why-this-bug-is-hard-to-catch" aria-label="Link to this section">#</a></h2>
<p>This isn't a logic error in the traditional sense. The happy path is correct — when compaction succeeds, the store works perfectly. The bug only manifests when an I/O operation fails between two specific lines of code. That makes it invisible to every standard testing technique:</p>
<ul>
<li><strong>Unit tests</strong> only see the happy path. Compaction works, data looks right, test passes. Nobody writes a test that says "now make the disk full between these two lines."</li>
<li><strong>Integration tests</strong> run on healthy local SSDs. The I/O failure that triggers this bug simply never happens in CI.</li>
<li><strong>Fuzz testing</strong> throws random keys and values at the API. It doesn't fuzz the filesystem. You could fuzz for a year and never trigger ENOSPC at the right moment.</li>
<li><strong>Code review</strong> reads "clear old state, write new state" as perfectly reasonable cleanup. Its very easy to miss the two-line gap between remove and write.</li>
</ul>
<p>The proof catches it because it reasons about <em>all reachable states</em>, not just the states that tests happen to visit. It asks: "is there any point during compaction where the invariant doesn't hold?" And it finds one.</p>
<h2 id="what-this-means">What this means<a class="h-anchor" href="#what-this-means" aria-label="Link to this section">#</a></h2>
<p>Tests are essential, and the 45-test torture suite we built provides real value as ongoing regression coverage. But tests exercise the code with working disks, successful writes, and cooperative hardware. The bugs that matter in production — the ones that cause silent data loss at 3 AM when a disk fills up — live in failure paths that no test in our suite could reach.</p>
<p>Formal verification works differently. Rather than running the code and checking outputs, it reasons about the full semantics of the program. The Verus proof examined every reachable state during compaction and determined that the invariant is violated at a specific program point. It did not need to simulate a disk failure to reach that conclusion.</p>
<p>Neither technique subsumes the other. Tests caught bugs that the proofs did not cover (like the missing file lock and the scan panic on empty input). The proof caught a bug that tests could not reach. In practice, the two approaches are complementary: tests cover breadth across the API surface, while proofs cover depth on critical invariants. Using both together gives stronger assurance than either one alone.</p>
<p>The full proof trees are available for Aretta <a href="https://code.aretta.ai/shared/b6af8620-2f4f-4f9e-af6c-71a6fc1d6219">here</a> and Claude Code <a href="https://code.aretta.ai/shared/5d3222f1-94b4-4ca6-92d6-f550f13629ed">here</a>.</p>
<hr>
<p><em>The full source code, proof skeletons, and fault injection tests are available on <a href="https://github.com/aretta-ai/lsm-experiment">GitHub</a>. If you're building infrastructure that needs stronger correctness guarantees than testing alone can provide, <a href="mailto:hello@aretta.ai">reach out</a>.</em></p>
<h2 id="appendix-proof-properties">Appendix: proof properties<a class="h-anchor" href="#appendix-proof-properties" aria-label="Link to this section">#</a></h2>
<p>The 13 machine-checked properties verified by Aretta's Verus proof skeletons:</p>
<table>
<thead>
<tr>
<th>Property</th>
<th>Formal</th>
</tr>
</thead>
<tbody>
<tr>
<td>Entry validity</td>
<td><code>is_delete ==> value.is_none()</code></td>
</tr>
<tr>
<td>Encoding roundtrip</td>
<td><code>decode(encode(n)) == n</code></td>
</tr>
<tr>
<td>Bloom filter</td>
<td><code>add(k) ==> probe(k) == true</code></td>
</tr>
<tr>
<td>Memtable ordering</td>
<td><code>get(k) == max_by_seq(entries_for_key(k))</code></td>
</tr>
<tr>
<td>Merge iterator</td>
<td><code>is_sorted(collect(mi))</code> and completeness</td>
</tr>
<tr>
<td>WAL-before-memtable</td>
<td><code>wal.sync()</code> happens-before <code>memtable.insert()</code></td>
</tr>
<tr>
<td>Flush ordering</td>
<td><code>sstable.finish()</code> happens-before <code>manifest.add()</code> happens-before <code>wal.rotate()</code></td>
</tr>
<tr>
<td>Compaction ordering</td>
<td><code>output.finish()</code> happens-before <code>manifest.swap()</code> happens-before <code>delete(inputs)</code></td>
</tr>
<tr>
<td>Tombstone correctness</td>
<td><code>delete(k) &#x26;&#x26; compact() ==> get(k) == None</code></td>
</tr>
<tr>
<td>Compaction transparency</td>
<td><code>forall k: get_before(k) == get_after(k)</code></td>
</tr>
<tr>
<td>Latest-write-wins</td>
<td><code>put(k,v1); put(k,v2) ==> get(k) == v2</code></td>
</tr>
</tbody>
</table>]]></content:encoded>
    </item>
  </channel>
</rss>
