概要
JACKZ.AI 公正性検証は独立した検証ページです。プラットフォームが各抽選記録に検証用データを公開し、プレイヤーがそれを検証ページに貼り付けると、ページがブラウザ内でその1回を再計算し、結果を表示します。プレイヤー自身が入力して「検証する」を押すので、結果を自分の手で確認できます。
プラットフォームは SDK も API も不要です。計算はすべてプレイヤーのブラウザ内で行われ、データはサーバーに送られません。入力の手間を減らしたい場合はワンクリックリンク(「検証リンク v1」参照)を追加できますが、必須ではありません。
用語
ドキュメントと検証ページでは、以下の英語の用語を使います。
- Server Seed
- プラットフォームが生成するランダムな文字列。32 バイトの暗号論的乱数を16進数で表すことを推奨します。シード交換まで秘密にします。
- Server Seed Hash
- SHA-256(Server Seed) の16進小文字の文字列。抽選前に公開します。
- Client Seed
- プレイヤーのシード。プレイヤーが設定するか、プラットフォームがランダムに生成します。大文字・小文字を区別します。
- Nonce
- 同じシードの組における抽選の通し番号。10進整数で、1回抽選するごとに1増えます。
- Draw
- 同じ Nonce 内で何回目の抽選か。0 から始まります。10連ガチャは1つの Nonce を共有し、Draw 0〜9 で区別できます。
- Roll
- 上記のデータから算出される 32 ビット符号なし整数。範囲は 0〜4,294,967,295 です。
- 確率(weights)
- Pick 方式における各賞の整数の重み。一番くじはその1枚を引く時点での各賞の残り枚数を使います。
- Threshold
- Threshold 方式の成功ゾーン上限。Roll がこれより小さければ成功です。
- Seed Rotation
- シード交換:旧 Server Seed を公開すると同時に新しいシードの Hash を公開し、以降の抽選は新しいシードで行います。
- プラットフォーム手数料
- プラットフォームの手数料率。商品ごとに設定し、いつでも変更できます。小数で表し、8% は 0.08、手数料なしは 0 と書きます。カード合成は成功率の計算に使います。任意の検証リンクでのみ検証ページに渡し、プレイヤーが入力することはありません。
導入手順
以下の7ステップをすべて実施して初めて、プレイヤーが各抽選を独立して確認できます。
- 抽選前に Server Seed Hash を公開Server Seed を生成し、プレイヤーが抽選する前にその Hash を表示します。可能であればオンチェーンへの記録や証跡保存も行い、以後変更できないようにします。
- プレイヤーが Client Seed を設定できるようにする初期値はランダム生成で構いませんが、プレイヤーが自分で変更できる必要があります。プレイヤーが Client Seed を変更した際は、新しい Server Seed への交換もあわせて行うことを推奨します。
- 抽選ごとに次の Nonce を使う1回抽選するごとに Nonce を1増やします。再利用、番号飛ばし、結果を理由にした引き直しは禁止です。すべての Nonce に記録が必要です。
- 抽選前に確率を公開Pick 方式は各賞の重みを、一番くじは各賞の残り枚数を公開します。Threshold 方式は成功率または計算方法を公開します。
- すべての抽選記録を保存client_seed、nonce、draw、weights、ranges または threshold、roll、result を、記録番号と一緒に保存します。
- Seed Rotation 時に旧シードを公開プレイヤーが「シード交換」を押したとき、またはシードの期限が来たときに旧 Server Seed を公開し、新しいシードの使用を開始します。
- 記録に検証データを公開する各記録に Server Seed Hash、Server Seed(交換後)、Client Seed、Nonce、Draw、その抽選の確率または成功率を表示し、できればそれぞれにコピーボタンを付けます。横に検証ページへの通常のリンクを置きます。データを付ける必要はありません。
アルゴリズム
どの方式もまず Roll を算出し、方式に従って Roll を結果に変換します。すべて整数演算で行い、浮動小数点は使いません。
Roll
block = HMAC-SHA256(key = server_seed, message = "{client_seed}:{nonce}:{floor(draw / 8)}")
roll = block[4 × (draw mod 8) .. +4] をビッグエンディアン(big-endian)の符号なし 32 ビット整数として読む
block
roll
- server_seed は UTF-8 の文字列のままキーとして使います。先に16進数からバイト列へ変換しないでください。
- コロンは半角。nonce と floor(draw / 8) は10進数で、ゼロ埋めしません。
- HMAC ブロック1つは 32 バイトで、8 回分の Draw に使えます。Draw 8〜15 は2つ目のブロックを使い、以降も同様です。
Threshold:成功/失敗(カード合成)
success = roll < threshold
成功率 = threshold ÷ 4,294,967,296
threshold = floor(素材カード市場価格合計 × (1 − edge_rate) × 2^32 ÷ 目標カード市場価格)
- 市場価格は10進数の文字列とし、分数で計算して最後に一度だけ切り捨てます。素材カードと目標カードは同じ通貨にしてください。
- threshold が 4,294,967,296 以上の場合は必ず成功します。
- プラットフォームが別の合成式を使う場合は threshold だけを公開します。市場価格パラメータは任意の検証リンクでのみ使います。
- 成功率を直接公開する形式(カード強化など)は threshold = floor(成功率 × 2^32) とし、同じく分数で計算します。例えば成功率 0.35 なら threshold は 1,503,238,553 です。
Pick:賞品抽選(オリパ、一番くじ、ガチャ、ブラインドボックス)
W = すべての重みの合計
ticket = floor(roll × W ÷ 4,294,967,296)
result = 0 番目から重みを累積し、累積値が初めて ticket を超えた賞の番号
Pick(確率区間)
賞品表の確率を小数の区間で表すプラットフォームは、この方式を使います。検証ページでは「賞品抽選(確率区間で指定)」を選びます。
f = round(roll ÷ 4,294,967,296, 12)、さらに round(f, 6)
result = begin ≤ f ≤ end を満たす最初の区間の番号(0 から数え、begin と end を含む)
- 丸めはどちらも四捨五入です。13 桁目が 5 以上なら切り上げ、6 桁のときも同じです。
- 区間は小数で 6 桁まで。例:0.000000-0.000999,0.001000-0.019999。
- プラットフォームが浮動小数点で計算すると、12 桁目がちょうど 5 になるごく一部の Roll で 1 桁ずれることがあります。整数または10進数で計算すれば避けられます。
抽選形式の対応
| 抽選形式 | 計算方式 | 仕組み |
|---|---|---|
| オリパ Oripa | Pick | 公開された確率に従ってカードを抽選します。 |
| 一番くじ Ichiban Kuji | Pick | 各賞の残り枚数を重みとし、1枚引くごとに更新します。 |
| ガチャ Gacha | Pick | レアリティ別の確率に従ってキャラクターやカードを抽選します。 |
| ブラインドボックス Blind Box | Pick | 種類別の確率で、どの種類が出るかを決めます。 |
| ガシャポン Gashapon | Pick | カプセルごとに種類別の確率で抽選します。 |
| 福袋 Lucky Bag | Pick | ランク別の確率で福袋の中身を決めます。 |
| ミステリーボックス Mystery Box | Pick | 公開された賞品リストから中身を抽選します。 |
| パック開封 Pack Opening | Pick | カード枠ごとに Draw を1つ使い、レアリティの確率(重みまたは確率区間)で抽選します。階層のあるパックは階層ごとに weights、labels、Draw を1組公開し、同じ Nonce を使って Draw を「枠 → 階層」の順に連番にします(例:5 枠 × 各 2 階層なら Draw 0〜9)。最初の階層の weights は各レアリティの重みで、次の階層の weights と labels は、前の階層で抽選されたレアリティ内のカードです。 |
| BOX開封シェア Box Break | Pick | チームや枠をランダムに割り当てます。次の抽選の weights と labels からは、当選済みの項目を外します。 |
| 抽選 Raffle | Pick | 応募者全員が同じ重みで、購入権を抽選します。複数名を抽選するときも、当選済みの人を外します。 |
| 抽選キャンペーン Lucky Draw | Pick | キャンペーン賞品を当選枠と重みに従って抽選します。 |
| カード合成 Card Upgrade | Threshold | Roll がプラットフォームの記録にある Threshold より小さければ成功です。成功率はプラットフォームが決めて公開します。 |
| カード強化 Enhancement | Threshold | 公開された成功率で強化の成否を決めます。 |
- roll × W は 2^53 を超えることがあるため、JavaScript では BigInt を使ってください。
- 各 ticket に対応する Roll の数の差は最大 1 です。W が 1,000,000 以下なら、各賞の確率の相対誤差は 0.03% 未満です。
- 一番くじは1枚引くごとに、重みが新しい残り枚数に変わります。記録には、その1枚を引いた時点の重みを公開してください。
検証リンク v1(任意)
この節は任意です。データ入力済みの検証ページをワンクリックで開かせたい場合にだけ必要です。記録にデータを表示してプレイヤーに入力してもらうだけなら、この節は不要です。
リンクは検証ページの URL、#、パラメータで構成されます。値はすべて URL エンコードが必要です。検証ページは未知のパラメータを無視します。
プラットフォーム手数料、確率、賞品表、市場価格は、その商品・その1回の設定どおりにリンクへ入れます。検証ページに既定値はありません。設定を変えた場合(例えばプラットフォーム手数料を 5% から 8% に変更)も、以降のリンクに新しい数値を入れるだけで、検証ページの更新は不要です。
必須
- v
- 形式のバージョン。現在は 1。
- server_seed_hash
- 抽選前に公開された SHA-256(server_seed)、16進数。
- client_seed
- プレイヤーの Client Seed。
- nonce
- 10進整数。
シード交換後に必須
- server_seed
- 公開済みの Server Seed の原文。シードが使用中の場合は含めないでください。検証ページがシード交換後に検証するようプレイヤーに案内します。
プラットフォームの記録
- result
- プラットフォームが記録した結果。Threshold は 1(成功)または 0(失敗)、Pick は賞の番号(0 から開始)。result がない場合、検証ページは再計算結果のみを表示し、「合格」は表示しません。
- roll
- 任意:プラットフォームが記録した Roll。
- draw
- 任意:同じ Nonce 内で何回目の抽選か。0 から開始、省略時は 0。
Pick 方式
- game=pick
- game を省略した場合、weights があれば Pick として扱います。
- weights
- Pick には weights か ranges のどちらかが必要です。weights は各賞の整数の重みで、カンマ区切り、最大 1,000 項目です。
- ranges
- 確率区間は weights の代わりに使います。各区間を begin-end で書き、カンマで区切ります。小数は 6 桁まで、区間は重ならないようにしてください。game には range を指定します。
- labels
- 任意:賞の名前。| 区切り、順番は weights または ranges と同じ。
Threshold 方式
- game=threshold
- 成功/失敗。
- threshold
- 成功ゾーンの上限。10進数の整数で 0〜4,294,967,296。Roll が threshold より小さければ成功です。threshold がない場合、検証ページは下の3つの市場価格パラメータから計算します。
- input_value
target_market_price
edge_rate - カード合成:素材カードの市場価格合計、目標カードの市場価格、プラットフォーム手数料の3つをまとめて渡します。市場価格を渡すときは edge_rate が必須で、手数料なしは 0 と書きます。どれか1つでも欠けると検証ページは検証を拒否し、0 などの値を勝手に補いません。edge_rate は 0 以上 1 未満の小数(例:0.05)で、そうでない場合も検証を拒否します。input_value と target_market_price は数字と小数点1つだけの10進数文字列(例:41.80)で、カンマ、符号、指数表記は使えません。target_market_price は 0 より大きくしてください。threshold も渡した場合は、両者が一致するかを確認します。
- currency
- 任意:金額の単位。プラットフォームが入力し、市場価格の横にそのまま表示されます。
- inputs
target - 素材カードの正式名(| 区切り、1枚につき1つ)と目標カードの正式名。結果に表示されます。
表示用
- game_type
- 抽選形式。例:オリパ、一番くじ、ガシャポン、カード合成。結果の上部に表示されます。
- platform
order_id - プラットフォーム名と記録番号。結果の上にテキストで表示されます。
- chain_url
- オンチェーン記録の URL。https のみ受け付けます。
- input_ids
target_id - カード ID。順番は inputs と同じ。
コード例
3つのバージョンは同じアルゴリズムで、言語の組み込み関数だけを使い、下のテストベクトルで確認済みです。PHP 版のカード合成 threshold は bcmath 拡張を使います。
// JACKZ.AI Fair Verify v1:Node.js 18 以上、組み込みモジュールのみ使用
import { createHash, createHmac } from "node:crypto";
const SPACE = 2n ** 32n;
// 抽選前に公開:SHA-256(Server Seed)、16進小文字
export function seedHash(serverSeed) {
return createHash("sha256").update(serverSeed, "utf8").digest("hex");
}
// draw 番目の Roll:0〜4,294,967,295
export function roll(serverSeed, clientSeed, nonce, draw = 0) {
const block = createHmac("sha256", serverSeed)
.update(`${clientSeed}:${nonce}:${Math.floor(draw / 8)}`)
.digest();
return BigInt(block.readUInt32BE((draw % 8) * 4));
}
// Threshold:Roll が threshold 未満なら成功
export function isSuccess(r, threshold) {
return r < BigInt(threshold);
}
// カード合成の threshold。金額は10進数の文字列で渡し、浮動小数点誤差を避ける
export function upgradeThreshold(inputValue, targetPrice, edgeRate) {
if (!/^0(\.\d+)?$/.test(edgeRate ?? "")) throw new Error("edge_rate は必須で、0 以上 1 未満の小数。手数料なしは 0");
if (![inputValue, targetPrice].every((s) => /^\d+(\.\d+)?$/.test(s ?? ""))) throw new Error("市場価格は10進数の数字にしてください");
const frac = (s) => {
const [i, f = ""] = String(s).split(".");
return [BigInt(i + f), 10n ** BigInt(f.length)];
};
const [a, ad] = frac(inputValue), [t, td] = frac(targetPrice), [e, ed] = frac(edgeRate);
return (a * (ed - e) * SPACE * td) / (ad * ed * t);
}
// Pick:weights は各賞の整数の重み(一番くじは各賞の残り枚数)。賞の番号を返す
export function pick(r, weights) {
const w = weights.map(BigInt);
const ticket = (r * w.reduce((x, y) => x + y, 0n)) / SPACE;
let cum = 0n;
for (let i = 0; i < w.length; i++) {
cum += w[i];
if (ticket < cum) return i;
}
throw new Error("weights の合計は 0 より大きくなければなりません");
}
// ワンクリック検証リンク(Link Format v1)
export function verifyLink(base, params) {
return `${base}#${new URLSearchParams({ v: "1", ...params })}`;
}# JACKZ.AI Fair Verify v1:Python 3.8 以上、標準ライブラリのみ使用
import hashlib
import hmac
import re
from fractions import Fraction
from math import floor
from urllib.parse import urlencode
SPACE = 2 ** 32
def seed_hash(server_seed: str) -> str:
"""抽選前に公開:SHA-256(Server Seed)、16進小文字"""
return hashlib.sha256(server_seed.encode()).hexdigest()
def roll(server_seed: str, client_seed: str, nonce: int, draw: int = 0) -> int:
"""draw 番目の Roll:0〜4,294,967,295"""
message = f"{client_seed}:{nonce}:{draw // 8}".encode()
block = hmac.new(server_seed.encode(), message, hashlib.sha256).digest()
offset = (draw % 8) * 4
return int.from_bytes(block[offset:offset + 4], "big")
def is_success(r: int, threshold: int) -> bool:
"""Threshold:Roll が threshold 未満なら成功"""
return r < threshold
def upgrade_threshold(input_value: str, target_price: str, edge_rate: str) -> int:
"""カード合成の threshold。金額は10進数の文字列で渡し、浮動小数点誤差を避ける"""
if not re.fullmatch(r"0(\.\d+)?", edge_rate or ""):
raise ValueError("edge_rate は必須で、0 以上 1 未満の小数。手数料なしは 0")
if not all(re.fullmatch(r"\d+(\.\d+)?", s or "") for s in (input_value, target_price)):
raise ValueError("市場価格は10進数の数字にしてください")
return floor(Fraction(input_value) * (1 - Fraction(edge_rate)) * SPACE / Fraction(target_price))
def pick(r: int, weights: list) -> int:
"""Pick:weights は各賞の整数の重み(一番くじは各賞の残り枚数)。賞の番号を返す"""
ticket = r * sum(weights) // SPACE
cum = 0
for i, w in enumerate(weights):
cum += w
if ticket < cum:
return i
raise ValueError("weights の合計は 0 より大きくなければなりません")
def verify_link(base: str, **params) -> str:
"""ワンクリック検証リンク(Link Format v1)"""
return base + "#" + urlencode({"v": 1, **params})<?php
// JACKZ.AI Fair Verify v1:PHP 8.5.12 以上(64 ビット)、カード合成には bcmath 拡張が必要
const FV_SPACE = 4294967296; // 2^32
// 抽選前に公開:SHA-256(Server Seed)、16進小文字
function fv_seed_hash(string $serverSeed): string
{
return hash('sha256', $serverSeed);
}
// $draw 番目の Roll:0〜4,294,967,295
function fv_roll(string $serverSeed, string $clientSeed, int $nonce, int $draw = 0): int
{
$block = hash_hmac('sha256', $clientSeed . ':' . $nonce . ':' . intdiv($draw, 8), $serverSeed, true);
return unpack('N', substr($block, ($draw % 8) * 4, 4))[1];
}
// Threshold:Roll が threshold 未満なら成功
function fv_is_success(int $roll, int $threshold): bool
{
return $roll < $threshold;
}
// カード合成の threshold。金額は10進数の文字列、bcmath が必要。edge_rate は必須、0 以上 1 未満、手数料なしは '0'
// 3つの金額の小数桁は合計18桁以内に。そうすれば積が正確になります
function fv_upgrade_threshold(string $inputValue, string $targetPrice, string $edgeRate): string
{
if (!preg_match('/^0(\.\d+)?$/', $edgeRate)) throw new InvalidArgumentException('edge_rate は必須で、0 以上 1 未満の小数。手数料なしは 0');
foreach ([$inputValue, $targetPrice] as $s) if (!preg_match('/^\d+(\.\d+)?$/', $s)) throw new InvalidArgumentException('市場価格は10進数の数字にしてください');
$n = bcmul(bcmul($inputValue, bcsub('1', $edgeRate, 18), 18), '4294967296', 18);
return bcdiv($n, $targetPrice, 0); // scale 0 で切り捨て(金額はすべて正の数)
}
// Pick:$weights は各賞の整数の重み(一番くじは各賞の残り枚数)。賞の番号を返す
// 重みの合計は 2^31 未満にすること。超えると $roll * 合計 が 64 ビット整数の範囲を超えるため、GMP または bcmath を使う
function fv_pick(int $roll, array $weights): int
{
$ticket = intdiv($roll * array_sum($weights), FV_SPACE);
$cum = 0;
foreach (array_values($weights) as $i => $w) {
$cum += $w;
if ($ticket < $cum) {
return $i;
}
}
throw new InvalidArgumentException('weights の合計は 0 より大きくなければなりません');
}
// ワンクリック検証リンク(Link Format v1)
function fv_verify_link(string $base, array $params): string
{
return $base . '#' . http_build_query(['v' => 1] + $params);
}テストベクトル
導入前に、以下の数値で実装を確認してください。すべての数値は検証ページで再計算できます。
Roll
| nonce | draw | roll |
|---|---|---|
| 1 | 0 | 2079044362 |
| 5 | 0 | 441912824 |
| 1 | 8 | 4185532748 |
| 1 | 9 | 2271708478 |
Threshold
| input_value | target_market_price | edge_rate | threshold |
|---|---|---|---|
| 40 | 100 | 0.05 | 1632087572 |
| 30.00 | 100.00 | 0.05 | 1224065679 |
| 50 | 95 | 0.05 | 2147483648 |
| 40 | 100 | 0.08 | 1580547964 |
| 41.80 | 108.50 | 0 | 1654650995 |
| 40 | 100 | (なし) | 検証を拒否 |
| 40 | 100 | 1 | 検証を拒否 |
Pick
| roll | weights | result |
|---|---|---|
| 441912824 | 1,4,20,75 | 2 |
| 2079044362 | 1,4,20,75 | 3 |
Pick(確率区間)
| roll | f(6 桁) | ranges | result |
|---|---|---|---|
| 1163190975 | 0.270827 | 0.000000-0.270826, 0.270827-1.000000 | 1 |
| 1163190975 | 0.270827 | 0.000000-0.270827, 0.270828-1.000000 | 0 |
| 2166811 | 0.000505 | 0.000000-0.000504, 0.000505-1.000000 | 1 |
検証の範囲
検証ページが照合するのは「結果がコミットされたシードから算出されたこと」です。以下に、証明できることと単独では証明できないことを示します。プラットフォームが公に説明する際は、このとおり正確に記載してください。
検証で証明できること
- この1回は入力された Server Seed、プレイヤーの Client Seed、Nonce から計算されており、その Server Seed は入力された Server Seed Hash と一致します。
- プラットフォームが記録した Roll と結果が、再計算と一致しています。
- 市場価格が含まれる場合、カード合成の成功率が市場価格とプラットフォーム手数料から算出した値と一致しています。
別途確認が必要なこと
- 確率が抽選前の公開内容と同じか:重みはプラットフォームが提供するため、プレイヤーは抽選前に見た確率と照らし合わせる必要があります。
- 番号飛ばしや引き直しの有無:「まとめて検証」で連続した Nonce を再計算し、プラットフォームの抽選履歴と1件ずつ照合します。
- 入力された Server Seed Hash が、プレイヤーが抽選前に見たものと同じかどうか。
- 市場価格そのものが妥当かどうか:市場価格とプラットフォーム手数料はプラットフォームが提供し、検証ページはその数値から計算した成功率だけを照合します。
- 一番くじの残り枚数が正しいか、賞品が実際に届けられたか。
バージョン
リンク内の v は形式のバージョンで、現行バージョンは 1 です。