devto 2026-06-15 원문 보기 ↗
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.
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.
UVS (Uncloned Verification Standard) moves the proof of fairness off trusted third-party certificates and onto something anyone can recompute themselves.
The core of the standard is one function, deriveTier. It assigns a draw a trust tier from the evidence actually attached, not from a badge:
No evidence, no green. The code decides, not a promise.
UVS proves that the published rules were followed on the published inputs using the published randomness. 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 outcome — and secures it completely; guarding the inputs (and KYC, licensing) is a separate control. Better to say it up front than oversell.
One seeded permutation. Hash a server seed with a public drand round (quicknet, 3s ticks), score every entrant, sort, deal the published pool onto that order:
combinedSeed = SHA-256( serverSeed : drandRandomness )
score(id) = SHA-256( combinedSeed : id ) // per participant
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.
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:
round = floor((now − genesis) / 3) + 1 // quicknet genesis = 1692803367
For 🟢 that isn't enough — you need a commitment anchor. The commitmentHash (without the round) is timestamped at two independent RFC-3161 TSAs (FreeTSA + DigiCert, different jurisdictions, in parallel), and the round is then derived from the proven stamp: R = roundAt(genTime)+1 — the first round strictly after the timestamp. So genTime < timeOfRound(R) holds by construction, and the operator never chooses R (nothing to grind). Verification is the spec's reference path:
openssl ts -verify -digest <commitmentHash> -in token.tsr -CAfile <ca>
"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 publicly re-derivable: 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.
Input-seeded commit-reveal: the outcome depends on a committed server seed plus the player's real-time moves. There's no external beacon in the physics loop, so outcome-binding (and 🟢) is impossible by construction.
The word "Uncloned" comes from here. The verification logic isn't baked in statically: per session, the registrar issues a regSeed, and the client builds a unique WASM module from it, in the browser, byte by byte.
buildWasm(regSeed) runs a deterministic LCG seeded by regSeed, draws a chain of 4–7 arithmetic steps (shifts + binary ops with random constants), then hand-emits a valid wasm binary — magic header 00 61 73 6d, the type/function/export/code sections, LEB128 numbers — exporting a compute(i32) -> i32:
function buildWasm(regSeed) {
const lcg = new LCG(regSeed);
const n = lcg.range(4, 7);
const steps = Array.from({ length: n }, () => ({
shiftOp: SHIFT_OPS[lcg.range(0, SHIFT_OPS.length)],
shiftAmt: 8 + lcg.range(0, 8),
binOp: BINARY_OPS[lcg.range(0, BINARY_OPS.length)],
constVal: (lcg.next() | 1) | 0,
}));
// ...emit wasm sections + the function body in LEB128...
return { bytes: new Uint8Array([0x00,0x61,0x73,0x6d, 0x01,0x00,0x00,0x00, /* ... */]) };
}
The client instantiates it, runs compute, gets a wasmResult; the registrar does the same with its regSeed and checks the result. Because the circuit is unique per session 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."
After a session the player hits ANCHOR, 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.
A note on the verifiers: the four byte-for-byte reference implementations (JS / Python / Java / C++) are for the draw; the physics game's determinism is checked by deterministic replay of the input log.
The crypto runs on a backend (Docker on Render) — one host carries both the game registrar and the anchored draws. Endpoints: /commit (server seed → commitmentHash → ×2 RFC-3161 in parallel → R = roundAt(genTime)+1 from the proven stamp), /reveal (fetch the round, run the draw, return the 🟢 record + serverSeed + drand + anchor), /anchor-record (notary for a game record). Crucially the host verifies each RFC-3161 token itself (openssl ts -verify, roots fetched from the TSAs at build, not from the operator) before granting 🟢; the commit time used is the verified genTime, never the caller's claim. deriveTier reads the tier off the facts.
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 (R = roundAt(genTime)+1), 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.
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 you don't have to trust the implementation: 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.
Everything is live, open (MIT) and decoupled:
openssl ts -verify.npm i @constarik/uvs-sdk
I'd love a tear-down of deriveTier, the dual-TSA commitment, and the derived-R rule (R = roundAt(genTime)+1) — where does it break?