@rolly-dev/auth-sign
v1.0.0
Published
Pure-TypeScript Poseidon2 hashing, provably-fair game math, Schnorr (EcGFp5) & EIP-712 signing for Rolly ZK-Rollup — bit-exact with the Rust circuit, no WASM required
Maintainers
Readme
@rolly-dev/auth-sign
Pure-TypeScript replacement for @rolly-dev/wasm-signer on the frontend
(rolly-interface/apps/web, rolly-interface/apps/explorer).
WASM fails to load on some devices and under strict CSP (no wasm-unsafe-eval).
This package re-implements the browser-facing surface of the signer in plain
TypeScript so the dApp works everywhere, with no init() step. The backend
and orchestrator keep using @rolly-dev/wasm-signer (Node, where WASM works).
Correctness contract: hashes and payouts are consumed by proofs already settled on L1. Every function here must be bit-exact with its Rust counterpart (circuit /
games-core/tx-validator). Implementations are validated against golden vectors generated from Rust.
Scope
- Poseidon2 over the Goldilocks field, seed / prediction /
user_seed_bindinghashes. - Provably-fair game math (dice, coinflip, limbo, crash, plinko, keno) — no Blackjack.
- Client-side Schnorr (EcGFp5) signing and EIP-712
KeyRegistrationtyped data.
Nonce lanes (anti-replay)
The account leaf holds one nonce, split into two independent counters:
nonce = account_lane · 2^40 + game_lane. A signature never commits to the
packed value — only to the lane its tx type owns, which the backend hands out
separately:
| Transaction | build_tx_hash.signed_nonce / EIP-712 nonce | Backend field |
|---|---|---|
| in_house_bet, crash_settle, set_provider_allowance | game lane | gameNonce |
| withdrawal, transfer | account lane | accountNonce |
| key_register (EIP-712) | account lane | accountNonce |
Bets bump only the game lane, so a withdrawal signed at request time stays valid
however many bets land before it is approved. Withdrawals / transfers /
allowance changes also sign a zero seed_commit — only bets bind the fairness
seed.
Provider allowance (set_provider_allowance, tx 13)
Provider bets (tx 2, placed by a game provider through the operator) carry no
owner signature. What the owner signs instead is an absolute provider
allowance — the total providers may debit; each provider bet decrements it,
a provider win does not restore it, 0 revokes it:
import { build_tx_hash, TX_SET_PROVIDER_ALLOWANCE, PROVIDER_ALLOWANCE_MAX } from '@rolly-dev/auth-sign';
const tx_hash = build_tx_hash({
tx_type: TX_SET_PROVIDER_ALLOWANCE,
user_id,
amount: 50_000_000n, // new absolute allowance (≤ PROVIDER_ALLOWANCE_MAX = 2^62 − 1)
game_id: 0,
prediction_hash: new BigUint64Array(4),
user_seed: new BigUint64Array(4),
old_seed_hash, crash_seed_hash, // ignored: zero seed_commit
signed_nonce: gameNonce, // GAME lane, like a bet
});An account without a registered Schnorr key cannot sign an allowance, so every
non-zero provider bet against it is rejected (ProviderAllowanceExceeded) — by
design.
Install
pnpm add @rolly-dev/auth-signreact is an optional peer dependency (only needed for the /react hook).
This package does not depend on viem: eip712_key_registration_typed_data
returns a plain typed-data object that the app passes to its own wallet
signTypedData (viem/wagmi) — signing stays in the consumer.
Usage
Synchronous — no async initialization:
import { poseidon2_hash, string_to_user_seed } from '@rolly-dev/auth-sign';
const seed = string_to_user_seed('my-seed-123');
const hash = poseidon2_hash(seed);React hook (ready is always true — no WASM to load):
import { useRollySigner } from '@rolly-dev/auth-sign/react';
function Component() {
const { ready, compute_roll_dice } = useRollySigner();
// `ready` is always true.
}Note: identifiers here are intentionally not named after "wasm" (e.g.
useRollySigner, notuseRollyWasm) — this package contains no WebAssembly, and reusing the old name would be misleading. Migrating call sites rename the hook alongside the import path.
Build
pnpm build # tsup → dist/ (ESM + CJS + .d.ts)
pnpm typecheck # tsc --noEmitGolden-vector conformance tests
Correctness is enforced by golden vectors generated from the authoritative Rust — the only way to guarantee bit-exactness with proofs already settled on L1. The two pieces:
Generator —
prover/circuit/examples/gen_golden_vectors.rsdumps JSON vectors from all four reference sources:tx-validator/src/hash.rs(Poseidon2- derived hashes),
games-core(payout math),crypto/ecgfp5/native.rs(curve + Schnorr) andcrypto/eip712.rs(KeyRegistration hashing). Vectors are deterministic (fixed RNG seed). Regenerate with:
npm run vectors # → test/vectors/*.json (committed, do not hand-edit)- derived hashes),
Harness —
test/*.test.ts(Node's built-in runner, TypeScript stripped at runtime) loadtest/vectors/*.jsonand compare against the TS implementation:npm testEach layer's test auto-skips while its function is still a scaffold stub, so the suite is green today and every implementation flip turns its vectors on automatically.
test/_selfcheck.test.tsguards that skip/enforce behavior. Field elements travel as decimal strings in JSON and becomebigintin TS.
Status
Scaffold. The implementation layers (Goldilocks field, Poseidon2, seed/prediction utilities, game payout math, EcGFp5 Schnorr, tx-hash + EIP-712) are ported incrementally; every exported function currently throws until its layer lands.
