Provably Fair
투명성과 증명 가능한 공정성을 보장하기 위해 각 멀티플레이어 게임에 대해 1,000만 개의 SHA256 해시 체인을 생성했습니다. 서버 비밀에서 시작하여 SHA256의 출력이 1,000만 번 반복적으로 자신에게 피드백되었습니다.
Platform의 최종 해시 SHA256: eeb4d340bfaed702601b9a08faba2f8088c4810c3e3a883293accaf25053b883
Crash의 최종 해시 SHA256: c46f0705f6ba4df891ce44accda8f89f5a6fa8b987eb7c7b445280cf6cabfbc6
Battle의 최종 해시 SHA256: 63cb1eb64ec4eb4de02f6bad85e42365d08f37497a1b7e907bd0b9183f27e1b3
여기에 공개함으로써 대체 SHA256 체인을 선택할 수 없게 방지합니다. 이제 게임 서버는 이 해시 체인을 역순으로 재생하며, 게임 결과를 증명 가능하게 공정하게 계산하기 위해 이 값들을 사용합니다.
1. 비밀 초기 해시: 각 멀티플레이어 게임 유형은 고유한 개인 초기 해시(서버만 알고 있음)를 가지며, 고유한 해시 체인의 루트를 형성합니다.
2. 해시 체인 생성: 초기 비밀 해시에서 SHA256을 반복적으로 적용하여 1,000만 개의 해시 체인을 생성합니다.
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. 이 설계는 미래의 해시를 위조하는 것을 불가능하게 하지만, 누구나 이전의 모든 해시를 확인할 수 있게 합니다.
예측 불가능성: 미래의 게임 결과는 서버에서도 알거나 영향을 미칠 수 없습니다.
검증 가능성: 플레이어는 공개적으로 공개된 최종 해시만 사용하여 이전 라운드의 무결성을 확인할 수 있습니다.
게임 격리: 각 게임 유형은 자체 체인을 가지고 있어 게임 간 조작을 방지합니다.
싱글플레이어 게임에서 증명 가능한 공정성을 보장하기 위해, 당사 플랫폼은 결과 시드를 생성하기 위한 세 가지 주요 구성 요소를 기반으로 한 시스템을 사용합니다:
1. 클라이언트 시드: 사용자가 제어하는 시드입니다. 사용자 등록 시 자동으로 생성되지만 사용자는 언제든지 변경할 수 있습니다. 클라이언트 시드를 변경하면 새로운 서버 시드 생성이 시작됩니다.
2. 서버 시드: 사용자에게 공개되지 않는 내부 매개변수를 사용하여 서버에서 생성됩니다. 현재 클라이언트 시드에 고유하게 바인딩됩니다. 각 클라이언트 시드에는 해당하는 서버 시드가 있습니다. 사용자가 클라이언트 시드를 변경하면 이전 서버 시드가 검증 목적으로 공개됩니다.
3. Nonce: 베팅 타임스탬프(밀리초)에 기반한 고유 번호입니다. 반복된 조치도 다른 결과를 생성하도록 보장합니다.
이 세 값은 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개의 숫자를 선택합니다. 생성된 게임 시드를 사용하여 하나의 주사위 굴림을 시뮬레이션합니다. 굴린 숫자가 사용자 선택 중 하나와 일치하면 사용자가 승리합니다.
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
}