Provably Fair
Para garantizar transparencia y equidad comprobable, hemos generado una cadena de 10 millones de hashes SHA256 para cada juego multijugador, comenzando con un secreto del servidor que ha sido alimentado repetidamente con la salida de SHA256 nuevamente en sí mismo 10 millones de veces
El SHA256 del hash final para Platform es: eeb4d340bfaed702601b9a08faba2f8088c4810c3e3a883293accaf25053b883
El SHA256 del hash final para Crash es: c46f0705f6ba4df891ce44accda8f89f5a6fa8b987eb7c7b445280cf6cabfbc6
El SHA256 del hash final para Battle es: 63cb1eb64ec4eb4de02f6bad85e42365d08f37497a1b7e907bd0b9183f27e1b3
Al publicarlo aquí, estamos evitando la posibilidad de elegir una cadena SHA256 alternativa. Ahora el servidor de juegos está reproduciéndose a través de esta cadena de hashes en orden inverso, utilizando estos valores para calcular los resultados de manera demostrativamente justa.
1. Hash Inicial Secreto: Cada tipo de juego multijugador tiene su propio hash inicial privado (conocido solo por el servidor), formando la raíz de una cadena hash única.
2. Generación de Cadena Hash: A partir del hash secreto inicial, generamos una cadena de 10.000.000 de hashes aplicando SHA256 repetidamente.
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. Consumo Inverso: Revelamos el hash final en la cadena al público. Cada ronda de juego consume los hashes en orden inverso: La Ronda 1 usa hash[10_000_000], La Ronda 2 usa hash[9_999_999], y así sucesivamente...
4. Este diseño hace imposible falsificar hashes futuros, mientras permite a cualquiera verificar todos los hashes anteriores.
Impredecibilidad: Los resultados futuros del juego no pueden ser conocidos o influenciados, ni siquiera por el servidor.
Verificabilidad: Los jugadores pueden verificar la integridad de cualquier ronda anterior utilizando solo el hash final revelado públicamente.
Aislamiento de Juegos: Cada tipo de juego tiene su propia cadena, evitando manipulación entre juegos.
Para garantizar equidad comprobable en juegos de un solo jugador, nuestra plataforma utiliza un sistema basado en tres componentes principales para generar la semilla del resultado:
1. Semilla del Cliente: Esta es una semilla controlada por el usuario. Se genera automáticamente cuando un usuario se registra, pero el usuario puede cambiarla en cualquier momento. Cambiar la semilla del cliente inicia la generación de una nueva semilla del servidor.
2. Semilla del Servidor: Es generada por el servidor utilizando parámetros internos que no se divulgan al usuario. Está vinculada únicamente a la semilla del cliente actual. Para cada semilla del cliente, hay una semilla del servidor correspondiente. Cuando el usuario cambia la semilla del cliente, la semilla del servidor anterior se revela con fines de verificación.
3. Nonce: Un número único basado en la marca de tiempo de la apuesta (en milisegundos). Garantiza que incluso las acciones repetidas produzcan resultados diferentes.
Estos tres valores se combinan y se codifican usando HMAC-SHA256 para generar la semilla de juego final, que luego se usa para determinar el resultado del juego.
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
}
El servidor proporciona la versión codificada de la semilla del servidor actual antes de realizar cualquier apuesta.
Después de que el usuario cambia su semilla de cliente, se revela la semilla del servidor utilizada anteriormente.
Esto permite a los usuarios verificar que todos los resultados generados con la semilla del servidor anterior fueron consistentes y justos.
En el juego de Dados, un usuario selecciona de 1 a 5 números (de 6 lados posibles). Se simula un único lanzamiento de dado usando la semilla de juego generada. Si el número lanzado coincide con una de las selecciones del usuario, el usuario gana.
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
}
En el juego de Minas, el objetivo es descubrir tantas fichas como sea posible sin tocar una mina. La colocación de minas se deriva de manera determinista utilizando una semilla de juego criptográficamente segura. Esto garantiza equidad y transparencia, permitiendo al usuario verificar que el tablero de juego no fue manipulado.
Entradas:
seed: una cadena hexadecimal de 64 caracteres derivada de la combinación de ClientSeed, ServerSeed y Nonce usando HMAC-SHA256.
numberOfMines: el número de minas a colocar en el tablero.
maxCells: número total de celdas en el tablero (por ejemplo, 25 para una cuadrícula de 5x5).
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
}Si numberOfMines = 3 y maxCells = 25, la función devolverá de manera determinista 3 índices de celda únicos (del 1 al 25) donde se colocan las minas.
En el juego de Plataformas, el resultado de cada ronda se determina por un hash público y verificable que es parte de una cadena hash publicada anteriormente.
Cada ronda utiliza un hash de la cadena para determinar la "última plataforma" — la plataforma que desaparecerá al final del juego.
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
}
Durante el juego, los jugadores se paran en diferentes plataformas. La plataforma que cae se selecciona al azar de las plataformas disponibles, asegurando que nunca sea la misma que la anterior.
En Rocket, el resultado de cada ronda (multiplicador de caída) se determina usando un hash público de la cadena hash pre-generada (única por juego). El resultado se calcula usando una función determinista que convierte el hash en un multiplicador.
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
}
Cada jugador recibe el hash antes de la ronda y puede verificar de forma independiente que el resultado coincide con la fórmula publicada, garantizando total transparencia y equidad comprobable.
En Battle, un jugador se selecciona como ganador en función del tamaño proporcional de su apuesta al fondo total. Esto significa que las apuestas más grandes resultan en mayores probabilidades de ganar.
Utilizamos un mecanismo de selección determinista y demostrativamente justo:
1. Se utiliza un hash de juego público para generar un número pseudoaleatorio en el rango [0.0, 1.0).
2. A cada jugador se le asigna un segmento del rango proporcional a su apuesta (por ejemplo, una apuesta del 20% cubre el 20% del rango).
3. El número aleatorio determina el segmento ganador, y por lo tanto, el ganador.
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
}