体育

游戏

综合

自定义游戏 - 可验证公平

如何在自定义游戏中使用

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,应用游戏的转换逻辑(参见“转换”部分),并将结果与您的投注进行对比。