<?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: Constantin Razinsky</title>
    <description>The latest articles on DEV Community by Constantin Razinsky (@constantin_razinsky_bb7f0).</description>
    <link>https://dev.to/constantin_razinsky_bb7f0</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3534404%2F55f4d5f7-60dc-4e8e-8c0b-b0daf95eb16f.jpg</url>
      <title>DEV Community: Constantin Razinsky</title>
      <link>https://dev.to/constantin_razinsky_bb7f0</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/constantin_razinsky_bb7f0"/>
    <language>en</language>
    <item>
      <title>UVS: a draw's fairness as a fact you can recompute — not a certificate you trust</title>
      <dc:creator>Constantin Razinsky</dc:creator>
      <pubDate>Sun, 14 Jun 2026 23:38:36 +0000</pubDate>
      <link>https://dev.to/constantin_razinsky_bb7f0/uvs-a-draws-fairness-as-a-fact-you-can-recompute-not-a-certificate-you-trust-54ec</link>
      <guid>https://dev.to/constantin_razinsky_bb7f0/uvs-a-draws-fairness-as-a-fact-you-can-recompute-not-a-certificate-you-trust-54ec</guid>
      <description>&lt;p&gt;I've built casino slot machines and gaming systems for 15 years. I mostly stayed away from compliance, but once I had to write the official algorithm description for a certification lab. I made it technically precise, handed it over — and realized nobody read or verified it. The lab ticked the boxes, took the money, issued a paper certificate. Two hours later a hotfix could ship to production and void the certified hash, and nobody would notice. The industry runs on dead paper, not real-time verification.&lt;/p&gt;

&lt;p&gt;And it isn't only casinos: any draw, lottery, gacha banner, school-place allocation or event-ticket raffle has the same hole. There's a "✓ Provably Fair" badge, a server seed, a hash — and almost nobody ever checks it, often including the operator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UVS (Uncloned Verification Standard)&lt;/strong&gt; moves the proof of fairness off trusted third-party certificates and onto something anyone can recompute themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  The invariant: a tier derived from evidence, not claimed
&lt;/h2&gt;

&lt;p&gt;The core of the standard is one function, &lt;code&gt;deriveTier&lt;/code&gt;. It assigns a draw a trust tier &lt;strong&gt;from the evidence actually attached&lt;/strong&gt;, not from a badge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔴 — no anchor, bare seed;&lt;/li&gt;
&lt;li&gt;🟡 — notary / self-anchor / a beacon binding without proof the commitment came first;&lt;/li&gt;
&lt;li&gt;🟢 — a neutral-registry signature, or trail-immutability, or outcome-binding &lt;strong&gt;with a proven&lt;/strong&gt; commitment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No evidence, no green. The code decides, not a promise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest scope first — the part most "provably fair" pitches skip
&lt;/h2&gt;

&lt;p&gt;UVS proves that &lt;strong&gt;the published rules were followed on the published inputs using the published randomness&lt;/strong&gt;. It does NOT prove the inputs themselves were honest: an operator can still enter phantom tickets or publish a prize pool that differs from what players were promised. UVS secures one link — the &lt;strong&gt;outcome&lt;/strong&gt; — and secures it completely; guarding the inputs (and KYC, licensing) is a separate control. Better to say it up front than oversell.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two branches, because randomness behaves differently by mechanic
&lt;/h2&gt;

&lt;h3&gt;
  
  
  uvLottery (draws / gacha) — can reach 🟢
&lt;/h3&gt;

&lt;p&gt;One seeded permutation. Hash a server seed with a public &lt;strong&gt;drand&lt;/strong&gt; round (quicknet, 3s ticks), score every entrant, sort, deal the published pool onto that order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;combinedSeed = SHA-256( serverSeed : drandRandomness )
score(id)    = SHA-256( combinedSeed : id )      // per participant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sort by score (descending, ties by id), deal prizes top-down. Same inputs → the same list, on any machine, in any language, forever. No hidden state.&lt;/p&gt;

&lt;p&gt;To stop the operator grinding seeds, the outcome binds to a drand round whose randomness doesn't exist yet at commit time. drand rounds are a deterministic function of time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;round&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="err"&gt;−&lt;/span&gt; &lt;span class="nx"&gt;genesis&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;     &lt;span class="c1"&gt;// quicknet genesis = 1692803367&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For 🟢 that isn't enough — you need a &lt;strong&gt;commitment anchor&lt;/strong&gt;. The commitmentHash (without the round) is timestamped at &lt;strong&gt;two independent RFC-3161 TSAs&lt;/strong&gt; (FreeTSA + DigiCert, different jurisdictions, in parallel), and the round is then &lt;strong&gt;derived from the proven stamp&lt;/strong&gt;: &lt;code&gt;R = roundAt(genTime)+1&lt;/code&gt; — the first round strictly after the timestamp. So &lt;code&gt;genTime &amp;lt; timeOfRound(R)&lt;/code&gt; holds &lt;strong&gt;by construction&lt;/strong&gt;, and the operator never chooses R (nothing to grind). Verification is the spec's reference path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl ts &lt;span class="nt"&gt;-verify&lt;/span&gt; &lt;span class="nt"&gt;-digest&lt;/span&gt; &amp;lt;commitmentHash&amp;gt; &lt;span class="nt"&gt;-in&lt;/span&gt; token.tsr &lt;span class="nt"&gt;-CAfile&lt;/span&gt; &amp;lt;ca&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"But a TSA is a trusted third party too." Yes — but a neutral one, and two in different jurisdictions make quiet collusion implausible. More importantly the whole chain is &lt;strong&gt;publicly re-derivable&lt;/strong&gt;: refetch the round, re-run the permutation in any language, verify the token. You aren't asked to trust me; you're handed the inputs.&lt;/p&gt;

&lt;h3&gt;
  
  
  uvGame (interactive physics) — honest ceiling is 🟡
&lt;/h3&gt;

&lt;p&gt;Input-seeded commit-reveal: the outcome depends on a committed server seed &lt;strong&gt;plus&lt;/strong&gt; the player's real-time moves. There's no external beacon in the physics loop, so outcome-binding (and 🟢) is impossible by construction.&lt;/p&gt;

&lt;h4&gt;
  
  
  The "Uncloned" layer: a WASM engine built on the fly
&lt;/h4&gt;

&lt;p&gt;The word "Uncloned" comes from here. The verification logic isn't baked in statically: per session, the registrar issues a &lt;code&gt;regSeed&lt;/code&gt;, and the client &lt;strong&gt;builds a unique WASM module from it, in the browser, byte by byte&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;buildWasm(regSeed)&lt;/code&gt; runs a deterministic LCG seeded by &lt;code&gt;regSeed&lt;/code&gt;, draws a chain of 4–7 arithmetic steps (shifts + binary ops with random constants), then hand-emits a valid wasm binary — magic header &lt;code&gt;00 61 73 6d&lt;/code&gt;, the type/function/export/code sections, LEB128 numbers — exporting a &lt;code&gt;compute(i32) -&amp;gt; i32&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildWasm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;regSeed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lcg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LCG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;regSeed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lcg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;range&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;7&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;shiftOp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;SHIFT_OPS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lcg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SHIFT_OPS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="na"&gt;shiftAmt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;lcg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;binOp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nx"&gt;BINARY_OPS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lcg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;BINARY_OPS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="na"&gt;constVal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lcg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="c1"&gt;// ...emit wasm sections + the function body in LEB128...&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mh"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mh"&gt;0x61&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mh"&gt;0x73&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mh"&gt;0x6d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mh"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mh"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mh"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client instantiates it, runs &lt;code&gt;compute&lt;/code&gt;, gets a &lt;code&gt;wasmResult&lt;/code&gt;; the registrar does the same with its &lt;code&gt;regSeed&lt;/code&gt; and checks the result. Because the circuit is &lt;strong&gt;unique per session&lt;/strong&gt; and derived from a seed, you can't pre-bake or stub it — to pass you must actually run the exact arithmetic the registrar emitted. That's the "uncloned" layer: not "trust our engine," but "here's exactly the engine we verify."&lt;/p&gt;

&lt;p&gt;After a session the player hits &lt;strong&gt;ANCHOR&lt;/strong&gt;, which notarizes the final game record at the same dual RFC-3161 TSAs — an immutable post-game ledger, honestly labeled 🟡. A 🟢 path via OpenTimestamps (Bitcoin trail-immutability) is coded but switched off until anyone needs it.&lt;/p&gt;

&lt;p&gt;A note on the verifiers: the four byte-for-byte reference implementations (JS / Python / Java / C++) are for the &lt;strong&gt;draw&lt;/strong&gt;; the physics game's determinism is checked by deterministic &lt;strong&gt;replay&lt;/strong&gt; of the input log.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;The crypto runs on a backend (Docker on Render) — one host carries both the game registrar and the anchored draws. Endpoints: &lt;code&gt;/commit&lt;/code&gt; (server seed → commitmentHash → ×2 RFC-3161 in parallel → &lt;code&gt;R = roundAt(genTime)+1&lt;/code&gt; from the proven stamp), &lt;code&gt;/reveal&lt;/code&gt; (fetch the round, run the draw, return the 🟢 record + serverSeed + drand + anchor), &lt;code&gt;/anchor-record&lt;/code&gt; (notary for a game record). Crucially the host &lt;strong&gt;verifies&lt;/strong&gt; each RFC-3161 token itself (&lt;code&gt;openssl ts -verify&lt;/code&gt;, roots fetched from the TSAs at build, not from the operator) &lt;strong&gt;before&lt;/strong&gt; granting 🟢; the commit time used is the verified &lt;code&gt;genTime&lt;/code&gt;, never the caller's claim. &lt;code&gt;deriveTier&lt;/code&gt; reads the tier off the facts.&lt;/p&gt;

&lt;h2&gt;
  
  
  On latency — honestly
&lt;/h2&gt;

&lt;p&gt;The anchored draw settles in a few seconds, and it isn't the crypto. The round it binds to is the first drand round after the proven timestamp (&lt;code&gt;R = roundAt(genTime)+1&lt;/code&gt;), so the wait is one beacon tick (~3s): the randomness simply doesn't exist when the seed is fixed. That short wait IS the anti-grinding property, not overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  On "an LLM wrote this"
&lt;/h2&gt;

&lt;p&gt;I'm a core Java developer; I know HTML/JS by hearsay and leaned heavily on LLMs for the browser and cloud parts. Normally that's a reason to distrust a security tool — except the whole point of UVS is that &lt;strong&gt;you don't have to trust the implementation&lt;/strong&gt;: the result is independently recomputable from public inputs in four languages. Whether an LLM or I wrote the front end is irrelevant to whether a draw is fair. The protocol is the product; the code is one expression of it.&lt;/p&gt;

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

&lt;p&gt;Everything is live, open (MIT) and decoupled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spec + verifiers (JS / Python / Java / C++): &lt;a href="https://github.com/constarik/uvs" rel="noopener noreferrer"&gt;https://github.com/constarik/uvs&lt;/a&gt; (lives at &lt;a href="https://uvs.uncloned.work" rel="noopener noreferrer"&gt;https://uvs.uncloned.work&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;/draw&lt;/strong&gt; — one page, In-browser 🟡 (client-side) vs Anchored 🟢 via the backend: &lt;a href="https://uvs.uncloned.work/draw" rel="noopener noreferrer"&gt;https://uvs.uncloned.work/draw&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PADDLA&lt;/strong&gt; — a physics arcade game: &lt;a href="https://paddla.uncloned.work" rel="noopener noreferrer"&gt;https://paddla.uncloned.work&lt;/a&gt; . Play, hit ANCHOR, verify the RFC-3161 token with &lt;code&gt;openssl ts -verify&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;SDK: &lt;code&gt;npm i @constarik/uvs-sdk&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd love a tear-down of &lt;code&gt;deriveTier&lt;/code&gt;, the dual-TSA commitment, and the derived-R rule (&lt;code&gt;R = roundAt(genTime)+1&lt;/code&gt;) — where does it break?&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>cryptography</category>
      <category>opensource</category>
      <category>security</category>
    </item>
  </channel>
</rss>
