自定义游戏 - 可验证公平
如何在自定义游戏中使用
YEET 原创游戏的每一次投注都始于一个回合种子(round seed)。回合种子由您的客户端种子(client seed)和投注随机数(nonce)经 HMAC-SHA256 算法生成,并以 YEET 选择的服务器种子(server seed)作为密钥。游戏随后从该回合种子中提取投注所需的所有随机值。在您投注前,我们会向您展示哈希处理后的服务器种子,并在您轮换种子对时揭晓原始值,以便您重新计算过去任何投注的回合种子,验证其结果在投注后未被篡改。
其他验证器可能不匹配
为其他赌场制作的验证网站生成随机数的方式可能不同,因此在那里检查 YEET 投注可能会得出不同结果。请在游戏“公平性”窗口的“验证”选项卡中,或使用本页面上的代码来验证 YEET 投注。
随机数生成
每笔投注分两步生成。首先,您的种子对和投注随机数生成一个回合种子。其次,该回合种子会产生一串随机字节流,游戏从中读取无偏的整数。相同的输入总是产生相同的数字,这就是结果可验证的原因。
1. 输入内容
- 由服务器生成的一个 64 位十六进制随机字符串
- 用户可随时修改的字符串
- 每进行一次游戏,该计数器会自动递增
- 游标 — 回合种子信息的最后一部分。它始终为 0。
2. 回合种子
回合种子是使用服务器种子(32 字节)作为密钥,对文本 clientSeed:nonce:0 进行 HMAC-SHA256 计算得出的。结果为 32 字节,显示为 64 位十六进制字符。每一笔投注有且仅有一个回合种子。
async function deriveGameSeed(serverSeed, clientSeed, nonce) {
const seedBytes = hexToBytes(serverSeed);
if (seedBytes.length !== 32) {
throw new Error(`Server seed must be 32 bytes (64 hex characters), got ${seedBytes.length}.`);
}
const key = await crypto.subtle.importKey(
'raw', toArrayBuffer(seedBytes), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
);
const message = new TextEncoder().encode(`${clientSeed}:${nonce}:0`);
return bytesToHex(new Uint8Array(await crypto.subtle.sign('HMAC', key, message)));
}3. 回合种子的熵
回合种子是第二个 HMAC-SHA256 的密钥。每次调用都会对一个 8 字节的计数器(小端序,从 0 开始)进行签名,并给出一个 32 字节的数据块;计数器每生成一个数据块增加 1。读取操作按顺序从当前数据块中获取字节。当读取所需的字节数超过当前数据块剩余字节时,剩余部分会被跳过,并生成下一个数据块。
class GameEntropy {
constructor(seedBytes) {
if (seedBytes.length !== 32) throw new Error('Seed must be exactly 32 bytes (64 hex characters).');
this.buffer = new Uint8Array(32);
this.offset = 32;
this.cursor = 0;
this.key = crypto.subtle.importKey(
'raw', toArrayBuffer(seedBytes), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
);
}
async next(count) {
if (count < 1 || count > 32) throw new RangeError('count must be between 1 and 32');
if (this.offset + count > 32) {
const message = new ArrayBuffer(8);
const view = new DataView(message);
view.setUint32(0, this.cursor >>> 0, true);
view.setUint32(4, Math.floor(this.cursor / 0x100000000) >>> 0, true);
const hash = await crypto.subtle.sign('HMAC', await this.key, message);
this.buffer.set(new Uint8Array(hash));
this.cursor++;
this.offset = 0;
}
const slice = this.buffer.slice(this.offset, this.offset + count);
this.offset += count;
return slice;
}
}4. 无偏数字
结果以整数形式抽取。readUint32 将 4 个字节读取为无符号 32 位整数(小端序)。unbiasedUint(entropy, n) 返回一个 0 到 n − 1 之间的数字,每个数字的概率相同。当 n 是 2 的幂时,直接取余数。否则,它仅接受低于 32 位内 n 的最大倍数的值,若数值达到或超过该限值则重新读取,确保没有任何结果会被偏袒。
async function readUint32(entropy) {
const bytes = await entropy.next(4);
return new DataView(bytes.buffer, bytes.byteOffset, 4).getUint32(0, true);
}
async function unbiasedUint(entropy, range) {
if (range <= 0) throw new Error('Range must be positive');
if (range === 1) return 0;
if ((range & (range - 1)) === 0) {
return (await readUint32(entropy)) % range;
}
const maxValue = 0xffffffff;
const threshold = maxValue - (maxValue % range) - 1;
for (;;) {
const raw = await readUint32(entropy);
if (raw <= threshold) return raw % range;
}
}5. 辅助工具
function hexToBytes(hex) {
if (typeof hex !== 'string') throw new TypeError('Hex value must be a string.');
const clean = hex.trim().replace(/^0x/i, '');
if (clean.length % 2 !== 0) throw new Error('Hex value must have an even length.');
if (!/^[0-9a-fA-F]*$/.test(clean)) throw new Error('Hex value contains non-hex characters.');
const bytes = new Uint8Array(clean.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(clean.substring(i * 2, i * 2 + 2), 16);
}
return bytes;
}
function bytesToHex(bytes) {
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
}
function toArrayBuffer(bytes) {
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
}6. 验证投注
- 从您的投注记录中打开该笔投注,并复制客户端种子 (client seed)、随机数 (nonce) 以及哈希后的服务器种子 (hashed server seed)。
- 轮换您的种子对以揭示服务器种子,并检查其 SHA-256 哈希值是否与您投注前看到的哈希值一致。
- 使用服务器种子、客户端种子和随机数运行
deriveGameSeed以获取轮次种子。 - 从轮次种子中创建一个
GameEntropy,应用游戏的转换逻辑(参见“转换”部分),并将结果与您的投注进行对比。
