Afin de garantir la transparence et l'équité vérifiable, nous avons généré une chaîne de 10 millions de hachages SHA256 pour chaque jeu multijoueur, en commençant par un secret serveur dont la sortie SHA256 a été réinjectée en boucle sur elle-même 10 millions de fois.
Le SHA256 du hachage final pour Platform est : eeb4d340bfaed702601b9a08faba2f8088c4810c3e3a883293accaf25053b883
Le SHA256 du hachage final pour Crash est : c46f0705f6ba4df891ce44accda8f89f5a6fa8b987eb7c7b445280cf6cabfbc6
Le SHA256 du hachage final pour Battle est : 63cb1eb64ec4eb4de02f6bad85e42365d08f37497a1b7e907bd0b9183f27e1b3
En le publiant ici, nous empêchons toute possibilité de choisir une chaîne SHA256 alternative. Le serveur de jeu parcourt désormais cette chaîne de hachages dans l'ordre inverse, en utilisant ces valeurs pour calculer les résultats de jeu de manière vérifiablement équitable.
1. Hachage Initial Secret : Chaque type de jeu multijoueur possède son propre hachage initial privé (connu uniquement du serveur), formant la racine d'une chaîne de hachage unique.
2. Génération de la Chaîne de Hachage : À partir du hachage initial secret, nous générons une chaîne de 10 000 000 de hachages en appliquant SHA256 de manière répétée.
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. Consommation Inversée : Nous révélons publiquement le hachage final de la chaîne. Chaque round de jeu consomme les hachages dans l'ordre inverse : Le Round 1 utilise hash[10_000_000] Le Round 2 utilise hash[9_999_999] et ainsi de suite…
4. Cette conception rend impossible de falsifier de futurs hachages, tout en permettant à quiconque de vérifier tous les hachages précédents.
Imprévisibilité : Les résultats futurs des jeux ne peuvent être connus ni influencés, même par le serveur.
Vérifiabilité : Les joueurs peuvent vérifier l'intégrité de n'importe quel round précédent en utilisant uniquement le hachage final rendu public.
Isolation des jeux : Chaque type de jeu possède sa propre chaîne, empêchant toute manipulation croisée entre jeux.
Afin de garantir une équité vérifiable dans les jeux solo, notre plateforme utilise un système basé sur trois composants principaux pour générer la graine de résultat :
1. Graine Client : Il s'agit d'une graine contrôlée par l'utilisateur. Elle est générée automatiquement lors de l'inscription de l'utilisateur, mais celui-ci peut la modifier à tout moment. La modification de la graine client déclenche la génération d'une nouvelle graine serveur.
2. Graine Serveur : Elle est générée par le serveur à l'aide de paramètres internes non divulgués à l'utilisateur. Elle est liée de manière unique à la graine client actuelle. Pour chaque graine client, il existe une graine serveur correspondante. Lorsque l'utilisateur modifie la graine client, la graine serveur précédente est révélée à des fins de vérification.
3. Nonce : Un numéro unique basé sur l'horodatage de la mise (en millisecondes). Il garantit que même des actions répétées produisent des résultats différents.
Ces trois valeurs sont combinées et hachées à l'aide de HMAC-SHA256 pour générer la graine de jeu finale, qui est ensuite utilisée pour déterminer le résultat du jeu.
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
}
Le serveur fournit la version hachée de la graine serveur actuelle avant que toute mise ne soit effectuée.
Après que l'utilisateur a modifié sa graine client, la graine serveur précédemment utilisée est révélée.
Cela permet aux utilisateurs de vérifier que tous les résultats générés avec la graine serveur précédente étaient cohérents et équitables.
Dans le jeu Dice, l'utilisateur sélectionne de 1 à 5 numéros (parmi 6 faces possibles). Un seul lancer de dé est simulé à l'aide de la graine de jeu générée. Si le numéro tiré correspond à l'une des sélections de l'utilisateur, celui-ci gagne.
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
}
Dans le jeu Mines, l'objectif est de découvrir autant de cases que possible sans toucher de mine. Le placement des mines est déterminé de manière déterministe à l'aide d'une graine de jeu cryptographiquement sécurisée. Cela garantit l'équité et la transparence, permettant à l'utilisateur de vérifier que le plateau de jeu n'a pas été manipulé.
Entrées :
seed : une chaîne hexadécimale de 64 caractères dérivée de la combinaison de ClientSeed, ServerSeed et Nonce à l'aide de HMAC-SHA256.
numberOfMines : le nombre de mines à placer sur le plateau.
maxCells : le nombre total de cases sur le plateau (par exemple, 25 pour une grille 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 et maxCells = 25, la fonction retournera de manière déterministe 3 indices de cases uniques (de 1 à 25) où les mines sont placées.
Dans le jeu Platforms, le résultat de chaque round est déterminé par un hachage public et vérifiable faisant partie d'une chaîne de hachage précédemment publiée.
Chaque round utilise un hachage de la chaîne pour déterminer la « dernière plateforme » — la plateforme qui disparaîtra à la fin du jeu.
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
}
Pendant le jeu, les joueurs se tiennent sur différentes plateformes. La plateforme qui chute est sélectionnée aléatoirement parmi les plateformes disponibles, garantissant qu'elle n'est jamais la même que la précédente ayant chuté.
Dans Rocket, le résultat de chaque round (multiplicateur de crash) est déterminé à l'aide d'un hachage public issu de la chaîne de hachage pré-générée (unique par jeu). Le résultat est calculé à l'aide d'une fonction déterministe qui convertit le hachage en multiplicateur.
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
}
Chaque joueur reçoit le hachage avant le round, et peut vérifier de manière indépendante que le résultat correspond à la formule publiée, garantissant une transparence totale et une équité vérifiable.
Dans Battle, un joueur est sélectionné comme gagnant en fonction de la proportion de sa mise par rapport à la cagnotte totale. Cela signifie que des mises plus importantes entraînent des chances de gain plus élevées.
Nous utilisons un mécanisme de sélection déterministe et vérifiablement équitable :
1. Un hachage de jeu public est utilisé pour générer un nombre pseudo-aléatoire dans la plage [0,0, 1,0).
2. Chaque joueur se voit attribuer un segment de la plage proportionnel à sa mise (par exemple, une mise de 20 % couvre 20 % de la plage).
3. Le nombre aléatoire détermine le segment gagnant, et donc le gagnant.
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
}