@riprip-io/provably-fair
v0.2.0
Published
RipRip provably-fair library
Readme
@riprip-io/provably-fair
Isomorphic TypeScript implementation of the OPENv1 provably-fair RNG protocol. Used by the API (payment indexer, pack opening resolution), the admintool (verification tool, simulator), and the future proof verification page.
Protocol spec: docs/rng-v1.md — defines the cryptographic derivations, message format, and weighted selection algorithm.
Security audit: docs/audit-provably-fair-v0.2.md — cryptographic soundness review, fairness analysis, and hardening report.
Install
cd packages/provably-fair
npm installSingle runtime dependency: @noble/hashes (pure JS, audited, isomorphic).
API
Derivations
import { deriveUserKey, deriveClientSeedHash } from '@riprip-io/provably-fair';
// user_key = SHA256("userkey:v1" || uuid_16_bytes)
const userKey: Uint8Array = deriveUserKey('550e8400-e29b-41d4-a716-446655440000');
// client_seed_hash = SHA256("clientseed:v1" || utf8(seed))
const seedHash: Uint8Array = deriveClientSeedHash('my-custom-seed');RNG Resolution
import { resolveOpen, resolveOpenBatch } from '@riprip-io/provably-fair';
import type { DrawTable } from '@riprip-io/provably-fair';
const drawTables: DrawTable[] = [
{
drawId: 1,
drawsPerOpen: 1,
items: [
{ sku: 'v1:card-a:raw:none:EN', weight: 700 },
{ sku: 'v1:card-b:raw:none:EN', weight: 300 },
],
},
];
// Resolve a single pack opening
const result = resolveOpen(
serverSecret, // Uint8Array (32 bytes, from epoch reveal)
20260215, // epochId (YYYYMMDD)
userKey, // Uint8Array (32 bytes)
1n, // purchaseNonce (bigint)
packConfigHash, // Uint8Array (32 bytes)
0, // openIndex
clientSeedHash, // Uint8Array (32 bytes)
drawTables,
);
// → { openIndex: 0, draws: [{ openIndex, drawIndex, ticket, selectedItem, rejectionSamples }] }
// Resolve a batch (multiple packs from one purchase)
const batch = resolveOpenBatch(
serverSecret, epochId, userKey, purchaseNonce,
packConfigHash, clientSeedHash, quantityN, drawTables,
);
// → { results: OpenResult[] }Epoch Commitment
import { commitEpoch, verifyEpoch } from '@riprip-io/provably-fair';
// commit_hash = SHA256(serverSecret)
const commitHash: Uint8Array = commitEpoch(serverSecret);
// Verify: SHA256(revealedSecret) === publishedCommitHash
const isValid: boolean = verifyEpoch(revealedSecret, publishedCommitHash);Lower-Level Primitives
import { buildMessage, generateTicket, selectWeighted } from '@riprip-io/provably-fair';
// Build the 122-byte OPENv1 message
const message: Uint8Array = buildMessage(drawInput);
// Generate a ticket (0–999999) from HMAC-SHA256
const ticket: number = generateTicket(serverSecret, message);
// Bias-free weighted selection with rejection sampling
const result = selectWeighted(serverSecret, message, items);
// → { sku: string, retries: number }Types
| Type | Description |
|------|-------------|
| DrawInput | Inputs for one draw: epochId, userKey, purchaseNonce, packConfigHash, openIndex, drawIndex, clientSeedHash |
| WeightedItem | { sku: string, weight: number } |
| DrawTable | { drawId, drawsPerOpen, items: WeightedItem[] } |
| DrawResult | Result of one draw: openIndex, drawIndex, ticket, selectedItem, rejectionSamples |
| OpenResult | { openIndex, draws: DrawResult[] } |
| OpenBatchResult | { results: OpenResult[] } |
| EpochCommitment | { epochId, commitHash } |
How It Fits Together
Contract (on-chain) This package API (off-chain)
─────────────────── ──────────── ───────────────
OrderPaidV1 event ──→ resolveOpenBatch() ──→ packOpening +
clientSeedHash deriveUserKey() packOpeningResult
purchaseNonce deriveClientSeedHash() rows in DB
packConfigHash
epochId Epoch service
commitEpoch() ──→ epoch table
verifyEpoch() (commit/reveal)Commands
| Command | Description |
|---------|-------------|
| npm run build | Compile TypeScript |
| npm run test | Run tests (vitest, watch mode) |
| npm run test:run | Run tests once |
| npm run test:coverage | Run tests with coverage |
| npm run typecheck | tsc --noEmit |
Tests include frozen golden test vectors and adversarial edge-case coverage across 8 files.
