<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Wes Huber</title>
    <description>The latest articles on DEV Community by Wes Huber (@wbaxterh).</description>
    <link>https://dev.to/wbaxterh</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F786685%2F008bb1f2-7e1c-48db-92b4-61072b96fa4d.jpeg</url>
      <title>DEV Community: Wes Huber</title>
      <link>https://dev.to/wbaxterh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wbaxterh"/>
    <language>en</language>
    <item>
      <title>The Chain Is a Clock, Not a Witness: Proof of Priority for Skate Clips on Midnight</title>
      <dc:creator>Wes Huber</dc:creator>
      <pubDate>Wed, 09 Sep 2026 20:12:59 +0000</pubDate>
      <link>https://dev.to/wbaxterh/the-chain-is-a-clock-not-a-witness-proof-of-priority-for-skate-clips-on-midnight-2017</link>
      <guid>https://dev.to/wbaxterh/the-chain-is-a-clock-not-a-witness-proof-of-priority-for-skate-clips-on-midnight-2017</guid>
      <description>&lt;p&gt;Skateboarding has had proof for forty years. It is called the clip. Land something, film it, post it, done. Two things are breaking that right now.&lt;/p&gt;

&lt;p&gt;The first is old. Footage gets saved for video parts, sometimes for a year or more, and posting early to prove you did it first burns the clip. "Who did it first" arguments get settled by upload dates and by who has the louder crew.&lt;/p&gt;

&lt;p&gt;The second is new. AI video is about to make a clip of a kickflip down a twelve stair cost nothing to produce. When that lands, the clip stops being proof, and a culture that cares this much about realness will want a way to say "this is real footage, filmed here, that day."&lt;/p&gt;

&lt;p&gt;I build TrickBook, a skate app with a few hundred riders on the App Store, and I have been working on a Midnight feature called Claimed to handle both. This post is about the one idea in the design that makes a blockchain worth having here, and about a one-line bug I found in the official docs pattern while building it. Everything below runs on a local devnet today, and the commands to reproduce it are at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a commitment buys you
&lt;/h2&gt;

&lt;p&gt;A rider lands a trick. The app hashes the clip on the phone and builds a commitment over the clip hash, the trick, the spot, the capture time, a fresh salt, and a secret that never leaves the device:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;struct ClaimPreimage {
  tag: Bytes&amp;lt;32&amp;gt;,
  clipHash: Bytes&amp;lt;32&amp;gt;,
  trickId: Bytes&amp;lt;32&amp;gt;,
  spotId: Bytes&amp;lt;32&amp;gt;,
  capturedAt: Uint&amp;lt;64&amp;gt;,
  salt: Bytes&amp;lt;32&amp;gt;,
  secret: Bytes&amp;lt;32&amp;gt;
}

export circuit claimCommitment(
  secret: Bytes&amp;lt;32&amp;gt;, salt: Bytes&amp;lt;32&amp;gt;, clipHash: Bytes&amp;lt;32&amp;gt;,
  trickId: Bytes&amp;lt;32&amp;gt;, spotId: Bytes&amp;lt;32&amp;gt;, capturedAt: Uint&amp;lt;64&amp;gt;
): Bytes&amp;lt;32&amp;gt; {
  return persistentHash&amp;lt;ClaimPreimage&amp;gt;(ClaimPreimage {
    tag: pad(32, "tb:claim:v1"),
    clipHash: clipHash, trickId: trickId, spotId: spotId,
    capturedAt: capturedAt, salt: salt, secret: secret
  });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That 32-byte hash is the only thing that goes on chain:&lt;br&gt;
&lt;/p&gt;

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

export circuit seal(commitment: Bytes&amp;lt;32&amp;gt;): [] {
  claims.insert(disclose(commitment));
  claimCount.increment(1);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nobody can read a trick, a spot, a date, or a rider out of that. The clip stays on the phone. Later, when the part drops or somebody starts an argument, the rider can reveal the body and prove it matches, exactly once, with a nullifier. That part is the standard commit and reveal pattern, and Midnight's &lt;code&gt;example-bboard&lt;/code&gt; template gets you most of the way there.&lt;/p&gt;

&lt;p&gt;The interesting question is the date. If the rider says "I sealed this before March 1," what proves it? Not the phone. The capture time inside the commitment is just a number the phone supplied, and a phone clock can be set to anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The clock
&lt;/h2&gt;

&lt;p&gt;Every seal inserts one leaf into the Merkle tree, and every insert changes the tree's root. Midnight's &lt;code&gt;HistoricMerkleTree&lt;/code&gt; remembers the old roots as well as the current one, and &lt;code&gt;checkRoot&lt;/code&gt; accepts any root the tree has ever recorded.&lt;/p&gt;

&lt;p&gt;So the priority proof does not prove membership under the current root. It proves membership under the root that was current at a specific earlier block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export circuit provePriority(
  clipHash: Bytes&amp;lt;32&amp;gt;, trickId: Bytes&amp;lt;32&amp;gt;, spotId: Bytes&amp;lt;32&amp;gt;,
  capturedAt: Uint&amp;lt;64&amp;gt;, salt: Bytes&amp;lt;32&amp;gt;
): [Bytes&amp;lt;32&amp;gt;, Bytes&amp;lt;32&amp;gt;] {
  const secret = riderSecret();
  const c = claimCommitment(secret, salt, clipHash, trickId, spotId, capturedAt);
  const path = findClaimPath(c);
  assert(path.leaf == c, "path is not for this claim");
  assert(claims.checkRoot(disclose(merkleTreePathRoot&amp;lt;16, Bytes&amp;lt;32&amp;gt;&amp;gt;(path))), "no sealed claim matches");
  return [disclose(trickId), disclose(spotId)];
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the leaf was under the tree's root at block 12, it was sealed by block 12. The chain stamped block 12 with a time nobody can rewind. The rider discloses the trick, the spot, and a root. Not the clip, not the capture time, not which leaf is theirs.&lt;/p&gt;

&lt;p&gt;Think of a notary's ledger that gets photographed at the end of every day. If your entry is in Tuesday's photo, you signed by Tuesday, whatever date you wrote next to your name. The chain is the camera. It is not a witness to the trick. It is a clock.&lt;/p&gt;

&lt;p&gt;Two things I had to verify before trusting this, because the docs do not spell either out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the tree really keep every root?&lt;/strong&gt; Yes, until a contract calls &lt;code&gt;resetHistory()&lt;/code&gt;, which this one never does. The generated JavaScript for &lt;code&gt;claims.insert&lt;/code&gt; appends the new root into a history map and nothing ever removes one. And it holds live: after two seals the history held three roots, and a proof against the older one verified on chain after the current root had moved on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you build a proof against an old root when the witness only sees current state?&lt;/strong&gt; Do not fight the witness. The indexer's &lt;code&gt;queryContractState&lt;/code&gt; takes a block height, so the service fetches the contract state as of that block, derives the Merkle path from that snapshot with the generated &lt;code&gt;findPathForLeaf&lt;/code&gt;, and hands that path to the witness for one call. The circuit's own &lt;code&gt;checkRoot&lt;/code&gt; over recorded history is what keeps that safe.&lt;/p&gt;

&lt;p&gt;The end-to-end run on a local devnet, ten checks, about three minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PASS: seal A [block 12]
PASS: seal B advances the root [block 15]
PASS: root after A is still in claims.history() [3 roots recorded]
PASS: reveal A discloses the body [block 19]
PASS: reveal A again rejected with 'claim already revealed'
PASS: provePriority A against the historic root (blockHeight 12 from the indexer)
PASS: provePriority discloses trick and spot only [kickflip at el-toro]
PASS: attest by the verifier
PASS: attest without the verifier secret rejected with 'not the verifier'
E2E RESULT: PASS (10/10 checks)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The one-line bug
&lt;/h2&gt;

&lt;p&gt;Look at &lt;code&gt;provePriority&lt;/code&gt; again. This line was not in my first version, and it is not in the concept page I learned the pattern from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;assert(path.leaf == c, "path is not for this claim");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is why it matters. A witness is plain TypeScript that runs outside the circuit. Midnight's docs are clear that witnesses are not cryptographically verified, and that the contract has to validate whatever they return. &lt;code&gt;findClaimPath&lt;/code&gt; returns a Merkle path. Every leaf in that tree, and every path to it, is public data. So without the binding line, a rider can compute a commitment for a claim they never sealed, have their witness hand back somebody else's perfectly valid path, and pass the root check.&lt;/p&gt;

&lt;p&gt;In my previous project, a verified-purchase review contract, that meant anyone could post a review without a purchase. In the docs' authorized-commitments example, it means any secret gets past "not authorized." The membership proof was proving that some leaf exists, not that your leaf exists.&lt;/p&gt;

&lt;p&gt;The fix is the single assert. The test that proves it bites is short too: pin another rider's perfectly valid path into the witness and call &lt;code&gt;provePriority&lt;/code&gt;. The circuit rejects it with &lt;code&gt;path is not for this claim&lt;/code&gt; in a tenth of a second, during local execution, before anything reaches the proof server. A real proof takes about twenty seconds, so you can tell from the timing alone whether the guard ran.&lt;/p&gt;

&lt;p&gt;The honest part is that Midnight's own security guide already says so. It calls the binding assert "the security-critical line" and shows exactly this shape. The concept page where most people first meet Merkle membership does not have it in either example, and does not link to the guide. I have a docs PR going in for that, and I fixed the review contract the same day.&lt;/p&gt;

&lt;p&gt;Three other compiler rules I learned by getting errors, all stricter than the examples suggest: every circuit parameter that flows into a ledger write needs &lt;code&gt;disclose()&lt;/code&gt; at the point of use, even public-looking ones like a trick id; &lt;code&gt;Set.member()&lt;/code&gt; with a witness-derived key is itself a disclosure point, so disclose the nullifier once when you bind it; and &lt;code&gt;checkRoot&lt;/code&gt; on a witness-supplied path needs &lt;code&gt;disclose(merkleTreePathRoot(path))&lt;/code&gt;. Five compile iterations for the first contract, zero for the second.&lt;/p&gt;

&lt;h2&gt;
  
  
  What none of this proves
&lt;/h2&gt;

&lt;p&gt;The chain proves who committed to what, and when. It never says the trick happened. A commitment over a synthetic clip seals just as well as one over a real one. Whether the footage is real is a separate question with separate answers: recording inside the app with a hash chain over the encoder output, device attestation, a sensor trace at capture, trick recognition, matching keyframes against the spot's photos. That all lives off chain, and only a tier bitmask gets anchored back to the commitment by a verifier key fixed at deploy.&lt;/p&gt;

&lt;p&gt;Riders never see any of this. No wallet, no token, no word Midnight. The first version is custodial, the app's backend holds the wallet and generates proofs, and the docs say so plainly. Getting the proving onto the phone is the next track.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Claimed itself is not a standalone repo. It is a feature of TrickBook and ships inside it, so there is nothing to clone yet. The full design, including the parts about evidence and what the badges are allowed to claim, is in the TrickBook docs: &lt;a href="https://docs.thetrickbook.com/docs/features/claimed" rel="noopener noreferrer"&gt;https://docs.thetrickbook.com/docs/features/claimed&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What you can run is the contract that taught me this, which is the same shape: &lt;a href="https://github.com/wbaxterh/vouched" rel="noopener noreferrer"&gt;wbaxterh/vouched&lt;/a&gt;, verified-purchase reviews, a commitment tree plus a nullifier set. Same binding assert, same forged-path test that pins another buyer's valid path into the witness and watches the circuit refuse it. You need Node 24, Docker, and the Compact toolchain (&lt;code&gt;compact&lt;/code&gt; CLI 0.5.1 with compiler 0.31.1). From the contract directory, &lt;code&gt;npm run compact&lt;/code&gt; builds the circuits. From the CLI directory, &lt;code&gt;npm run e2e&lt;/code&gt; brings up the standalone devnet in Docker, deploys, and runs the flow end to end.&lt;/p&gt;

&lt;p&gt;If you are building anything with Merkle membership on Midnight, grep your circuits for &lt;code&gt;checkRoot&lt;/code&gt; and make sure each one has a &lt;code&gt;path.leaf ==&lt;/code&gt; next to it. It took me two contracts to notice.&lt;/p&gt;

</description>
      <category>midnight</category>
      <category>zeroknowledge</category>
      <category>blockchain</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Zero Knowledge Proofs: How to Win Every "Trust Me Bro" Argument With Math</title>
      <dc:creator>Wes Huber</dc:creator>
      <pubDate>Sat, 08 Aug 2026 15:06:31 +0000</pubDate>
      <link>https://dev.to/wbaxterh/zero-knowledge-proofs-how-to-win-every-trust-me-bro-argument-with-math-5cgf</link>
      <guid>https://dev.to/wbaxterh/zero-knowledge-proofs-how-to-win-every-trust-me-bro-argument-with-math-5cgf</guid>
      <description>&lt;p&gt;&lt;em&gt;A tutorial where you prove things without revealing things, and yes, the math actually maths.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here's something the internet doesn't want you to know: &lt;strong&gt;you overshare every single time you prove something.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Prove you're over 21 at a bar? You hand over a card with your name, your address, your height, and your terrible 2019 haircut. Prove your income to a landlord? Here's every transaction I've made since college, please don't judge the 3am food delivery.&lt;/p&gt;

&lt;p&gt;We built the entire digital world on a verification model that boils down to &lt;strong&gt;"here's everything, trust me bro."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not anymore. There's a branch of cryptography that lets you prove a statement is true while revealing &lt;em&gt;nothing else&lt;/em&gt;. It sounds fake. It's called a &lt;strong&gt;zero knowledge proof&lt;/strong&gt;, and by the end of this article you'll understand one well enough to check it with Python. Then we'll look at &lt;strong&gt;Midnight&lt;/strong&gt;, a blockchain that turned this party trick into a developer platform.&lt;/p&gt;

&lt;p&gt;Let's go. 🚀&lt;/p&gt;

&lt;h2&gt;
  
  
  🪪 The Trust Me Bro Problem
&lt;/h2&gt;

&lt;p&gt;Every verification system you use today works by &lt;strong&gt;disclosure&lt;/strong&gt;. You prove things by showing the underlying data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prove your age ➡️ show your whole ID&lt;/li&gt;
&lt;li&gt;Prove you can pay ➡️ show your bank statements&lt;/li&gt;
&lt;li&gt;Prove you're a real user ➡️ solve a CAPTCHA and sacrifice your data to the algorithm gods&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The data doesn't just get &lt;em&gt;seen&lt;/em&gt;. It gets &lt;strong&gt;stored&lt;/strong&gt;, and eventually it gets &lt;strong&gt;breached&lt;/strong&gt;, and then a guy named xX_darkweb_Xx is selling your identity for the price of a burrito.&lt;/p&gt;

&lt;p&gt;The verifier never needed the data. They needed &lt;strong&gt;one bit of information&lt;/strong&gt;: true or false. Everything else was collateral damage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In short:&lt;/strong&gt; we've been answering yes or no questions with our entire life story.&lt;/p&gt;

&lt;h2&gt;
  
  
  🕵️ The Party Trick That Started It All
&lt;/h2&gt;

&lt;p&gt;Zero knowledge proofs let a &lt;strong&gt;prover&lt;/strong&gt; convince a &lt;strong&gt;verifier&lt;/strong&gt; that a statement is true without revealing &lt;em&gt;why&lt;/em&gt; it's true.&lt;/p&gt;

&lt;p&gt;The classic example is Where's Waldo. Say I claim I found Waldo on the page and you don't believe me (fair, you've seen my code reviews). I could point at him, but then I've revealed the answer and ruined the puzzle.&lt;/p&gt;

&lt;p&gt;Instead, I take a giant piece of cardboard, way bigger than the book, cut a tiny Waldo shaped hole in it, and slide the page underneath so only Waldo shows through the hole.&lt;/p&gt;

&lt;p&gt;You see Waldo. ✅ You learn I found him. ✅ You learn &lt;strong&gt;absolutely nothing&lt;/strong&gt; about where he is on the page, because the cardboard hides all of it. ✅&lt;/p&gt;

&lt;p&gt;That's a zero knowledge proof. Every real ZK system, no matter how much math it's wearing, is doing the cardboard trick.&lt;/p&gt;

&lt;p&gt;The formal version has three properties, and knowing these three words will let you nod convincingly in any crypto conversation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Completeness&lt;/strong&gt;: if the statement is true, an honest prover convinces the verifier. The demo works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Soundness&lt;/strong&gt;: if the statement is false, no amount of galaxy brain trickery convinces the verifier. You can't fake it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero knowledge&lt;/strong&gt;: the verifier learns the statement is true and &lt;em&gt;nothing else&lt;/em&gt;. The cardboard stays intact.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Good job, concept unlocked! 💪 Now let's earn the "I understand the math" badge.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧮 The Math (This Is Where You Start Feeling Smart)
&lt;/h2&gt;

&lt;p&gt;Don't close the tab. The math is genuinely simple, it's just wearing a trench coat.&lt;/p&gt;

&lt;p&gt;Everything runs on &lt;strong&gt;clock math&lt;/strong&gt;, which mathematicians call modular arithmetic to keep it gatekept. On a 12 hour clock, 9 + 4 = 1. You wrap around. We write that as 9 + 4 ≡ 1 (mod 12). That's it. That's the whole trench coat.&lt;/p&gt;

&lt;p&gt;Here's the fun part. Pick a prime p and a number g. Computing y = g^x mod p is easy, your laptop does it instantly. But going &lt;em&gt;backwards&lt;/em&gt;, finding x when you only know y, is called the &lt;strong&gt;discrete logarithm problem&lt;/strong&gt;, and for big numbers it's computationally hopeless. Multiplying is easy, unmultiplying is a research career.&lt;/p&gt;

&lt;p&gt;So: &lt;strong&gt;x can be a secret, and y can be public.&lt;/strong&gt; Think of x as a private key and y as a public key. Now I'll prove I know x without ever showing it, using a protocol cryptographers named after Claus Schnorr (cryptographers name everything after themselves, it's their love language).&lt;/p&gt;

&lt;p&gt;The protocol is a three message dance between Peggy (&lt;strong&gt;P&lt;/strong&gt;rover) and Victor (&lt;strong&gt;V&lt;/strong&gt;erifier):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Commit.&lt;/strong&gt; Peggy picks a random number k and sends r = g^k mod p. This locks her in, like pushing a commit before the standup so everyone knows you did &lt;em&gt;something&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Challenge.&lt;/strong&gt; Victor sends back a random number c. This is the crucial part: Peggy couldn't predict it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Respond.&lt;/strong&gt; Peggy sends s = k + c · x. Her secret x is in there, but it's blended with the random k, like a password inside a hash. Unrecoverable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Victor then checks one equation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;g^s ≡ r · y^c (mod p)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If Peggy really knows x, the algebra works out perfectly every time (completeness). If she's bluffing, she'd need to answer a random challenge she couldn't predict, and she gets caught (soundness). And Victor only ever sees r, c, and s, which are statistically just noise (zero knowledge).&lt;/p&gt;

&lt;p&gt;Let's run it with real numbers, because "trust me bro" would be ironic here.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setup: p = 23, g = 5. Peggy's secret: x = 6. Public value: y = 5⁶ mod 23 = &lt;strong&gt;8&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Commit: Peggy picks k = 3, sends r = 5³ mod 23 = &lt;strong&gt;10&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Challenge: Victor sends c = &lt;strong&gt;4&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Respond: s = 3 + 4 · 6 = &lt;strong&gt;27&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Victor checks: is 5²⁷ mod 23 equal to 10 · 8⁴ mod 23?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Open a Python shell and verify it yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;20&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;
&lt;span class="mi"&gt;20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;20 == 20.&lt;/strong&gt; The math is mathing. Victor is convinced Peggy knows x, and if you scroll back, x = 6 never appeared in anything Victor saw. 🤯&lt;/p&gt;

&lt;p&gt;If you've made it this far you are a superstar ⭐️ and you now understand more actual cryptography than 99% of the people posting candle charts. Few understand this. You do.&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 From Party Trick to zkSNARK
&lt;/h2&gt;

&lt;p&gt;Our dance has two problems for the real world. Victor had to be online to throw challenges, and we only proved one tiny algebra fact.&lt;/p&gt;

&lt;p&gt;Modern systems fix both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kill the back and forth.&lt;/strong&gt; Instead of Victor picking the challenge, Peggy generates it by hashing her own commitment (the Fiat Shamir transform). The hash function becomes an incorruptible robot Victor. Now the proof is a single message anyone can verify, anytime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prove anything.&lt;/strong&gt; Any computation can be compiled into a giant system of equations like our little one. Your program becomes a circuit, and you prove "I ran this code correctly on secret inputs" the same way Peggy proved she knew x.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bundle that up and you get a &lt;strong&gt;zkSNARK&lt;/strong&gt;: a proof that's &lt;em&gt;succinct&lt;/em&gt; (a few hundred bytes even if the computation was enormous) and verifiable in milliseconds.&lt;/p&gt;

&lt;p&gt;"It works on my machine" finally has a fix. With a zkSNARK it provably works on &lt;strong&gt;every&lt;/strong&gt; machine, and you don't even have to show anyone the machine. 🐳&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In short:&lt;/strong&gt; you can now hand someone a receipt that proves a computation happened correctly, without showing the inputs. Which raises a question with billion dollar implications: what happens when you put that on a blockchain?&lt;/p&gt;

&lt;h2&gt;
  
  
  🌙 Enter Midnight
&lt;/h2&gt;

&lt;p&gt;Public blockchains have a privacy model best described as the "this is fine" dog sitting calmly in a burning room, laptop open to every transaction you've ever made, public, forever.&lt;/p&gt;

&lt;p&gt;Radical transparency is great for verifying money and &lt;em&gt;terrible&lt;/em&gt; for literally everything else. No company is putting payroll on a public ledger. No hospital is putting your records there. Transparency was blockchain's superpower and its adoption ceiling at the same time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Midnight&lt;/strong&gt; is a blockchain built to fix exactly this. It's a partner chain in the Cardano ecosystem designed around &lt;strong&gt;data protection&lt;/strong&gt;: smart contracts where zero knowledge proofs are not an exotic bolt on, they're the default way state gets updated. Your data stays on your device. The chain sees proofs.&lt;/p&gt;

&lt;p&gt;The part I love as a developer: you don't write moon math. You write &lt;strong&gt;Compact&lt;/strong&gt;, a TypeScript flavored language, and the toolchain compiles your logic into ZK circuits. Here's the vibe (simplified for the article, real syntax lives in their docs):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// the witness lives on YOUR device, never on chain&lt;/span&gt;
&lt;span class="nx"&gt;witness&lt;/span&gt; &lt;span class="nf"&gt;birthYear&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="nf"&gt;proveAdult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentYear&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Uint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentYear&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;birthYear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;must be an adult&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The chain learns "this person is an adult: true." Your birth year stays home. Peggy would be proud.&lt;/p&gt;

&lt;p&gt;What does that unlock? The use cases write themselves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Age and identity checks&lt;/strong&gt; that don't photocopy your life. Prove "over 18" or "licensed doctor" as one bit, not one dossier.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Financial privacy with receipts.&lt;/strong&gt; Prove solvency to a lender or an exchange without opening your books to the whole internet.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Healthcare eligibility.&lt;/strong&gt; Prove "vaccinated" or "matches this trial" while the medical record never leaves the building.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Compliance without surveillance.&lt;/strong&gt; Selective disclosure means a regulator with the right keys can audit you, while everyone else sees nothing. Regulated &lt;em&gt;and&lt;/em&gt; private, which used to be a contradiction.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Private voting and DAOs.&lt;/strong&gt; Prove your vote counted without revealing it. Governance without vote buying.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Enterprise logic on chain.&lt;/strong&gt; Supply chains and business rules where the &lt;em&gt;correctness&lt;/em&gt; is public but the trade secrets aren't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice the pattern. Every one of these is a yes or no question we currently answer with a full data dump. ZK flips the default: &lt;strong&gt;share the conclusion, keep the evidence.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🛠 Try It Yourself
&lt;/h2&gt;

&lt;p&gt;The best part is you can actually build with this today:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://docs.midnight.network" rel="noopener noreferrer"&gt;docs.midnight.network&lt;/a&gt; has the full developer guide and the Compact language reference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write a circuit:&lt;/strong&gt; the tutorial walks you through your first contract, and it genuinely feels like TypeScript, not like a math PhD hazing ritual.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go deeper:&lt;/strong&gt; the node itself is open source at &lt;a href="https://github.com/midnightntwrk/midnight-node" rel="noopener noreferrer"&gt;github.com/midnightntwrk/midnight-node&lt;/a&gt;. I've been contributing there lately and the codebase is a great read if you want to see how a privacy chain actually ships.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it! We're done yay 😁&lt;/p&gt;

&lt;h2&gt;
  
  
  🧾 The Recap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Current verification = trust me bro + oversharing.&lt;/li&gt;
&lt;li&gt;Zero knowledge proofs = the Waldo cardboard: prove the claim, hide everything else.&lt;/li&gt;
&lt;li&gt;You personally verified a Schnorr proof with two lines of Python. Flex accordingly.&lt;/li&gt;
&lt;li&gt;zkSNARKs make it succinct and universal.&lt;/li&gt;
&lt;li&gt;Midnight makes it programmable in a TypeScript flavored language, with use cases that read like a list of everything Web2 got wrong about data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cryptographers spent forty years building the cardboard. The chains are finally cheap and the tooling finally speaks TypeScript. The only missing ingredient is developers who see the use case sitting in their own industry.&lt;/p&gt;

&lt;p&gt;So: what can &lt;em&gt;you&lt;/em&gt; prove?&lt;/p&gt;

&lt;p&gt;If you build something with this, I genuinely want to hear about it. Drop a comment or find me on GitHub as wbaxterh.&lt;/p&gt;

&lt;p&gt;Peace out,&lt;/p&gt;

&lt;p&gt;Wes&lt;/p&gt;

</description>
      <category>cryptography</category>
      <category>blockchain</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
    <item>
      <title>When the exit code lies: fork retractions, lost transactions, and trusting the chain in midnight-node</title>
      <dc:creator>Wes Huber</dc:creator>
      <pubDate>Sat, 25 Jul 2026 14:58:57 +0000</pubDate>
      <link>https://dev.to/wbaxterh/when-the-exit-code-lies-fork-retractions-lost-transactions-and-trusting-the-chain-in-1b1h</link>
      <guid>https://dev.to/wbaxterh/when-the-exit-code-lies-fork-retractions-lost-transactions-and-trusting-the-chain-in-1b1h</guid>
      <description>&lt;p&gt;Your transaction tool reports &lt;code&gt;FAILED_TO_FINALIZE&lt;/code&gt;. Your CI marks the job red. Your retry logic fires. But the block explorer says the transaction finalized 22 seconds after you sent it.&lt;/p&gt;

&lt;p&gt;The exit code lied.&lt;/p&gt;

&lt;p&gt;This is a debugging story from &lt;a href="https://github.com/midnightntwrk/midnight-node" rel="noopener noreferrer"&gt;midnight-node&lt;/a&gt; — Midnight's Substrate-based node — about how a transaction watcher loses track of a transaction during a chain fork, why the failure is invisible in tests, and the pattern that fixes it: &lt;strong&gt;on timeout, ask the chain itself, not the stream that was supposed to tell you about it.&lt;/strong&gt; The fix landed as &lt;a href="https://github.com/midnightntwrk/midnight-node/pull/1927" rel="noopener noreferrer"&gt;PR #1927&lt;/a&gt; for &lt;a href="https://github.com/midnightntwrk/midnight-node/issues/1854" rel="noopener noreferrer"&gt;issue #1854&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Forks are normal, your tooling forgets that
&lt;/h2&gt;

&lt;p&gt;Midnight produces blocks with AURA every 6 seconds and finalizes them with GRANDPA a couple of blocks later. Between production and finality, the chain is allowed to disagree with itself: two validators can build competing blocks, and one branch eventually wins. A transaction included in the losing branch is &lt;em&gt;retracted&lt;/em&gt; — kicked back to the transaction pool — and normally re-included in a block on the winning branch a few seconds later.&lt;/p&gt;

&lt;p&gt;From the chain's perspective this is routine housekeeping. From your tooling's perspective, it's a trap.&lt;/p&gt;

&lt;p&gt;The toolkit's &lt;code&gt;send&lt;/code&gt; command watches a submitted transaction with &lt;a href="https://github.com/paritytech/subxt" rel="noopener noreferrer"&gt;subxt&lt;/a&gt;'s watch stream, which emits status events: &lt;code&gt;InBestBlock&lt;/code&gt;, &lt;code&gt;NoLongerInBestBlock&lt;/code&gt;, &lt;code&gt;InFinalizedBlock&lt;/code&gt;, and so on. The sender's logic was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Wait for the tx to appear in a best block.&lt;/li&gt;
&lt;li&gt;Then wait (with a timeout) for &lt;code&gt;InFinalizedBlock&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Timeout expires → report &lt;code&gt;FAILED_TO_FINALIZE&lt;/code&gt;, exit non-zero.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's the failure mode from the issue, reconstructed from logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;t+0s    tx submitted, InBestBlock (block A)
t+6s    fork: block A retracted → NoLongerInBestBlock
t+8s    tx re-included in block B on the winning branch
t+22s   block B finalized  ✅ tx is permanently on chain
t+60s   watcher's finalization timeout expires
        → exit: FAILED_TO_FINALIZE  ❌
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things went wrong. First, &lt;code&gt;NoLongerInBestBlock&lt;/code&gt; was being silently swallowed — the logs showed nothing at the moment the interesting thing happened. Second, after the retraction, the watch stream never surfaced the re-inclusion; the watcher waited out its timeout on a dead branch while the transaction quietly finalized elsewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a wrong exit code is worse than a crash
&lt;/h2&gt;

&lt;p&gt;If this tool only fed dashboards, a spurious failure would cost an engineer an eyebrow raise. But callers make decisions based on exit codes. The sharpest edge in our case: reward-claim transactions with &lt;em&gt;at-most-once&lt;/em&gt; semantics. A wrapper script sees the non-zero exit, assumes the claim never landed, and retries — resubmitting an operation that already succeeded. The exit code isn't diagnostics; it's an API, and it was returning wrong answers.&lt;/p&gt;

&lt;p&gt;This is a general lesson worth internalizing: &lt;strong&gt;any tool that reports transaction outcomes is part of someone's correctness argument.&lt;/strong&gt; Treat its outputs with the same rigor as consensus code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: the chain gets the last word
&lt;/h2&gt;

&lt;p&gt;The watch stream is a convenience, not a source of truth. The source of truth is the finalized chain, and it's sitting right there behind an RPC. So on watch timeout, the sender now scans finalized blocks, newest first, looking for the extrinsic hash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;find_in_finalized_chain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;MidnightNodeClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;extrinsic_hash_hex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_depth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="py"&gt;.rpc&lt;/span&gt;&lt;span class="nf"&gt;.chain_get_finalized_head&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.ok&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;max_depth&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="py"&gt;.rpc&lt;/span&gt;&lt;span class="nf"&gt;.chain_get_block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ext&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="py"&gt;.block.extrinsics&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;ext_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
                &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"0x{}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;sp_crypto_hashing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;blake2_256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="na"&gt;.0&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ext_hash&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;extrinsic_hash_hex&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hash_to_str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="py"&gt;.block.header.number&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="py"&gt;.block.header.parent_hash&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nb"&gt;None&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few design notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bounded depth&lt;/strong&gt; (64 blocks below the finalized head — comfortably past any plausible retraction-to-finalization window at 6-second blocks). An unbounded walk to genesis on a wrong hash would turn one bug into a different one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extrinsic identity is just a hash.&lt;/strong&gt; A Substrate extrinsic's hash is the blake2-256 of its encoded bytes, so the scan needs no indexer, no storage queries — fetch blocks, hash extrinsics, compare.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail open, loudly.&lt;/strong&gt; RPC errors during the scan log a warning and return &lt;code&gt;None&lt;/code&gt; — the sender then reports the timeout as before. The fallback can only &lt;em&gt;upgrade&lt;/em&gt; a false failure into a truthful success, never mask a real one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With the scan in place, the outcome reporting becomes honest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;finalized_block_hash&lt;/span&gt;&lt;span class="nf"&gt;.is_some&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;finalized&lt;/span&gt;&lt;span class="nf"&gt;.is_some&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"FINALIZED"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"FINALIZED_AFTER_RETRACTION"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"FAILED_TO_FINALIZE"&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;FINALIZED_AFTER_RETRACTION&lt;/code&gt; exits zero — because the transaction &lt;em&gt;is on chain&lt;/em&gt; — but it's a distinct message, so operators can see how often forks are eating their watch streams. And the retraction itself is no longer swallowed: the moment &lt;code&gt;NoLongerInBestBlock&lt;/code&gt; arrives, the sender logs &lt;code&gt;tx retracted from best block; watching for re-inclusion&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The timeouts also became configurable (&lt;code&gt;MN_SEND_BEST_BLOCK_TIMEOUT&lt;/code&gt; / &lt;code&gt;MN_SEND_FINALIZED_TIMEOUT&lt;/code&gt;, in seconds) for slow or fault-injected environments — as environment variables rather than CLI flags, partly to keep the change surface away from other in-flight CLI work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing what you can, admitting what you can't
&lt;/h2&gt;

&lt;p&gt;Here's the honest part: &lt;strong&gt;you cannot deterministically force a fork retraction in CI.&lt;/strong&gt; Retractions emerge from validator timing races; no RPC call produces one on demand. A test suite that claims to cover the retraction path end-to-end would be lying the same way the exit code was.&lt;/p&gt;

&lt;p&gt;What you &lt;em&gt;can&lt;/em&gt; do is split the fix so the untestable part is trivially small (a timeout branch calling one function) and the testable part carries the logic. The e2e test for the scan uses a trick worth stealing: &lt;strong&gt;every Substrate block already contains a transaction you didn't send&lt;/strong&gt; — the timestamp inherent that the block author injects. So the test:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;runs against a real node, grabs a finalized block, and takes its timestamp-inherent extrinsic;&lt;/li&gt;
&lt;li&gt;hashes those bytes and asserts &lt;code&gt;find_in_finalized_chain&lt;/code&gt; locates that extrinsic at exactly that block (positive case, using an extrinsic the test never had to submit);&lt;/li&gt;
&lt;li&gt;asserts a fabricated hash comes back &lt;code&gt;None&lt;/code&gt; after the bounded walk (negative case).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No fault injection, no mocked RPC — the scan is exercised against genuinely finalized blocks, and the only unverified wiring is a five-line &lt;code&gt;match&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Watch streams are best-effort; finality is a fact.&lt;/strong&gt; When a stream and the chain can disagree, reconcile against the chain before reporting failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exit codes are API.&lt;/strong&gt; If a caller might retry based on your answer, a false negative is a correctness bug, not a cosmetic one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never swallow the interesting event.&lt;/strong&gt; The silent &lt;code&gt;NoLongerInBestBlock&lt;/code&gt; was the difference between a 5-minute diagnosis and a mystery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bound your fallbacks.&lt;/strong&gt; A recovery path with no depth limit is a new incident waiting to happen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be honest about test coverage.&lt;/strong&gt; Shrink the untestable wiring instead of pretending the test forces the failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix is open for review at &lt;a href="https://github.com/midnightntwrk/midnight-node/pull/1927" rel="noopener noreferrer"&gt;midnightntwrk/midnight-node#1927&lt;/a&gt; — feedback welcome. If you're building on &lt;a href="https://midnight.network" rel="noopener noreferrer"&gt;Midnight&lt;/a&gt; and your tooling watches transactions, go check what your code does with &lt;code&gt;NoLongerInBestBlock&lt;/code&gt;. There's a decent chance the answer is "nothing," and now you know why that matters.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>blockchain</category>
      <category>substrate</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
