Provably Fair
透明性と証明可能な公平性を保証するため、各マルチプレイヤーゲームに対して1000万個のSHA256ハッシュのチェーンを生成しました。サーバーシークレットから始まり、SHA256の出力が1000万回繰り返して自身にフィードバックされています
Platformの最終ハッシュのSHA256は: eeb4d340bfaed702601b9a08faba2f8088c4810c3e3a883293accaf25053b883
Crashの最終ハッシュのSHA256は: c46f0705f6ba4df891ce44accda8f89f5a6fa8b987eb7c7b445280cf6cabfbc6
Battleの最終ハッシュのSHA256は: 63cb1eb64ec4eb4de02f6bad85e42365d08f37497a1b7e907bd0b9183f27e1b3
これをここに公開することにより、代替のSHA256チェーンを選択する可能性を排除しています。現在、ゲームサーバーはこのハッシュチェーンを逆順で再生し、これらの値を使用して、証明可能な形でゲーム結果を計算しています。
1. シークレット初期ハッシュ: 各マルチプレイヤーゲームタイプには独自のプライベート初期ハッシュ(サーバーのみが知っている)があり、ユニークなハッシュチェーンのルートを形成します。
2. ハッシュチェーン生成: 初期シークレットハッシュから、SHA256を繰り返し適用することで1000万個のハッシュのチェーンを生成します。
func HashSeed(seed string) string {
hash := sha256.Sum256([]byte(seed))
return hex.EncodeToString(hash[:])
}
func CalculateHashChain(firstHash string) []string {
hashes := make([]string, 10_000_000)
for i := int64(0); i <= 10_000_000; i++ {
firstHash = HashSeed(firstHash)
hashes[i] = firstHash
}
return hashes
}
3. 逆順消費: チェーンの最終ハッシュを公開します。各ゲームラウンドはハッシュを逆順で消費します: ラウンド1はhash[10_000_000]を使用し、ラウンド2はhash[9_999_999]を使用し、以下同様...
4. この設計により、将来のハッシュを偽造することは不可能ですが、誰もが以前のすべてのハッシュを検証できます。
予測不可能性: 将来のゲーム結果は、サーバーによってさえ知られたり影響を受けたりすることができません。
検証可能性: プレイヤーは、公開された最終ハッシュのみを使用して、以前のラウンドの完全性を検証できます。
ゲーム分離: 各ゲームタイプには独自のチェーンがあり、ゲーム間の操作を防ぎます。
シングルプレイヤーゲームで証明可能な公平性を確保するために、当プラットフォームは結果シードを生成するための3つの主要コンポーネントに基づくシステムを使用しています:
1. クライアントシード: これはユーザーが制御するシードです。ユーザー登録時に自動生成されますが、ユーザーはいつでも変更できます。クライアントシードの変更により、新しいサーバーシードの生成が開始されます。
2. サーバーシード: サーバーが内部パラメータを使用して生成され、ユーザーには開示されません。現在のクライアントシードに一意にバインドされています。各クライアントシードには対応するサーバーシードがあります。ユーザーがクライアントシードを変更すると、以前のサーバーシードが検証目的で開示されます。
3. ノンス: ベットのタイムスタンプ(ミリ秒単位)に基づいた一意の数値です。繰り返されたアクションでも異なる結果を生成することを保証します。
これら3つの値はHMAC-SHA256を使用して結合およびハッシュされ、最終的なゲームシードを生成します。その後、ゲームの結果を決定するために使用されます。
func GenerateUserGameSeed(userSeed string, userServerSeed string, nonce int64) (string, error) {
data := fmt.Sprintf("%s:%s:%d", userServerSeed, userSeed, nonce)
mac := hmac.New(sha256.New, []byte(data))
gameSeed := hex.EncodeToString(mac.Sum(nil))
return gameSeed, nil
}
サーバーは、任意のベットが行われる前に、現在のサーバーシードのハッシュ化されたバージョンを提供します。
ユーザーがクライアントシードを変更すると、以前に使用されていたサーバーシードが開示されます。
これにより、ユーザーは、以前のサーバーシードで生成されたすべての結果が一貫していて公平であることを検証できます。
サイコロゲームでは、ユーザーは(6つの可能な側面から)1〜5の数字を選択します。生成されたゲームシードを使用して単一のサイコロロールをシミュレートします。ロールされた数がユーザーの選択の1つと一致する場合、ユーザーが勝ちます。
func rollDice(seed string, selectedNumbers []int64) (int64, bool, error) {
if len(selectedNumbers) < 1 || len(selectedNumbers) > 5 {
return 0, false, fmt.Errorf("incorrect number of selected numbers: %d", len(selectedNumbers))
}
bigSeed, ok := new(big.Int).SetString(seed, 16)
if !ok {
return 0, false, fmt.Errorf("failed to convert seed to big.Int")
}
// Simulate dice roll: number from 1 to 6
dice := new(big.Int).Mod(bigSeed, big.NewInt(6))
diceFace := dice.Int64() + 1
for _, num := range selectedNumbers {
if num == diceFace {
return diceFace, true, nil
}
}
return diceFace, false, nil
}
マインゲームでは、目的は地雷に当たらずに可能な限り多くのタイルを発見することです。地雷の配置は、暗号化的に安全なゲームシードを使用して決定論的に導出されます。これにより公平性と透明性が保証され、ユーザーはゲームボードが操作されなかったことを検証できます。
入力:
seed: HMAC-SHA256を使用してClientSeed、ServerSeed、およびNonceの組み合わせから導出された64文字の16進数文字列。
numberOfMines: ボード上に配置する地雷の数。
maxCells: ボード上のセルの総数(例:5x5グリッドの場合は25)。
func generateMines(seedHex string, numberOfMines int) ([]int64, error) {
if numberOfMines > 25 {
return nil, fmt.Errorf("invalid number of mines: %d", numberOfMines)
}
var mines []int64
used := make(map[int]bool)
i := 0
for len(mines) < numberOfMines {
mac := hmac.New(sha256.New, []byte(seedHex))
mac.Write([]byte(fmt.Sprintf("mine-%d", i)))
sum := mac.Sum(nil)
val := binary.BigEndian.Uint32(sum)
pos := int(val%25) + 1
if !used[pos] {
used[pos] = true
mines = append(mines, int64(pos))
}
i++
}
return mines, nil
}numberOfMines = 3およびmaxCells = 25の場合、関数は地雷が配置されている3つの一意のセルインデックス(1〜25)を決定論的に返します。
プラットフォームゲームでは、各ラウンドの結果は、以前に公開されたハッシュチェーンの一部である公開で検証可能なハッシュによって決定されます。
各ラウンドはチェーンのハッシュを使用して「最後のプラットフォーム」を決定します—ゲーム終了時に消えるプラットフォーム。
func generateLastPlatform(hash string) (int64, error) {
h, err := hex.DecodeString(hash)
if err != nil {
return 0, err
}
number := binary.BigEndian.Uint64(h[:8])
rng := rand.New(rand.NewSource(int64(number)))
randomNumber := rng.Intn(25) + 1 // Platforms are numbered 1 through 25
return int64(randomNumber), nil
}
ゲームプレイ中、プレイヤーはさまざまなプラットフォームに立ちます。ドロップするプラットフォームは利用可能なプラットフォームからランダムに選択され、以前に削除されたものと同じではないことを確認します。
ロケットでは、各ラウンド結果(クラッシュマルチプライヤー)は事前生成されたハッシュチェーン(ゲームごとに一意)から公開ハッシュを使用して決定されます。結果は、ハッシュをマルチプライヤーに変換する決定論的な関数を使用して計算されます。
func generateMultiplier(hash string) (float64, error) {
const N = 40
bigH, ok := new(big.Int).SetString(hash, 16)
if !ok {
return 0, fmt.Errorf("failed to convert seed to big.Int")
}
mod := new(big.Int).Mod(bigH, big.NewInt(N))
if mod.Cmp(big.NewInt(0)) == 0 {
return 1, nil
}
if len(hash) < 13 {
return 0, fmt.Errorf("hash too short")
}
h13Str := hash[:13]
bigH13, ok := new(big.Int).SetString(h13Str, 16)
if !ok {
return 0, fmt.Errorf("failed to convert first 13 hex digits to big.Int")
}
// Compute 100 * (2^52 - h) / (2^52 - h)
e := new(big.Int).Lsh(big.NewInt(1), 52) // 2^52
hundred := big.NewInt(100)
hundredE := new(big.Int).Mul(hundred, e)
numerator := new(big.Int).Sub(hundredE, bigH13)
denom := new(big.Int).Sub(e, bigH13)
numFloat := new(big.Float).SetInt(numerator)
denomFloat := new(big.Float).SetInt(denom)
ratio, _ := new(big.Float).Quo(numFloat, denomFloat).Float64()
floored := math.Floor(ratio)
result := floored / 100.0
return result, nil
}
各プレイヤーはラウンド前にハッシュを受け取り、結果が公開された公式と一致することを独立して検証でき、完全な透明性と証明可能な公平性を保証します。
バトルでは、プレイヤーは総プール に対するベット額の比例サイズに基づいて勝者として選択されます。これは、より大きなベットがより高い勝利の確率をもたらすことを意味します。
決定論的で証明可能に公正な選択メカニズムを使用します:
1. 公開ゲームハッシュを使用して[0.0, 1.0)の範囲で疑似乱数を生成します。
2. 各プレイヤーには、ベット額に比例するレンジのセグメントが割り当てられます(例えば、20%のベットはレンジの20%をカバーします)。
3. ランダム数は勝利セグメントを決定し、したがって勝者を決定します。
func pickWinner(hash string, chances []float64) (int, error) {
if len(chances) == 0 {
return -1, fmt.Errorf("zero participants")
}
total := 0.0
for _, chance := range chances {
if chance < 0 {
return -1, fmt.Errorf("chance cannot be negative")
}
total += chance
}
if total < 99.99 || total > 100.01 {
return -1, fmt.Errorf("sum of chances must be 100%%, got: %.2f%%", total)
}
randomValue, err := deterministicFloatFromHash(hash)
if err != nil {
return -1, fmt.Errorf("random generation failed: %w", err)
}
cumulative := 0.0
for i, chance := range chances {
cumulative += chance / 100.0
if randomValue < cumulative {
return i, nil
}
}
return -1, fmt.Errorf("failed to pick winner")
}
func deterministicFloatFromHash(hash string) (float64, error) {
bytes, err := hex.DecodeString(hash)
if err != nil {
return 0, err
}
if len(bytes) < 8 {
return 0, fmt.Errorf("hash too short")
}
num := binary.BigEndian.Uint64(bytes[:8])
return float64(num) / float64(math.MaxUint64), nil
}