Για να διασφαλίσουμε διαφάνεια και αποδείξιμη δικαιοσύνη έχουμε δημιουργήσει μια αλυσίδα 10 εκατομμυρίων SHA256 hash για κάθε παιχνίδι πολλαπλών παικτών, ξεκινώντας με ένα μυστικό διακομιστή στο οποίο έχει τροφοδοτηθεί επανειλημμένα η έξοδος SHA256 πίσω στον εαυτό του 10 εκατομμύρια φορές
Το SHA256 του τελικού hash για το Platform είναι: eeb4d340bfaed702601b9a08faba2f8088c4810c3e3a883293accaf25053b883
Το SHA256 του τελικού hash για το Crash είναι: c46f0705f6ba4df891ce44accda8f89f5a6fa8b987eb7c7b445280cf6cabfbc6
Το SHA256 του τελικού hash για το Battle είναι: 63cb1eb64ec4eb4de02f6bad85e42365d08f37497a1b7e907bd0b9183f27e1b3
Δημοσιεύοντάς το εδώ αποτρέπουμε οποιαδήποτε δυνατότητα επιλογής εναλλακτικής αλυσίδας SHA256. Τώρα ο διακομιστής παιχνιδιού παίζει μέσα από αυτή την αλυσίδα hash σε αντίστροφη σειρά, χρησιμοποιώντας αυτές τις τιμές για να υπολογίσει τα αποτελέσματα του παιχνιδιού με αποδείξιμη δικαιοσύνη.
1. Μυστικό Αρχικό Hash: Κάθε τύπος παιχνιδιού πολλαπλών παικτών έχει το δικό του ιδιωτικό αρχικό hash (γνωστό μόνο στον διακομιστή), σχηματίζοντας τη ρίζα μιας μοναδικής αλυσίδας hash.
2. Δημιουργία Αλυσίδας Hash: Από το αρχικό μυστικό hash, δημιουργούμε μια αλυσίδα 10.000.000 hash εφαρμόζοντας επανειλημμένα SHA256.
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. Αντίστροφη Κατανάλωση: Αποκαλύπτουμε το τελικό hash στην αλυσίδα στο κοινό. Κάθε γύρος παιχνιδιού καταναλώνει τα hash σε αντίστροφη σειρά: Ο Γύρος 1 χρησιμοποιεί το hash[10_000_000] Ο Γύρος 2 χρησιμοποιεί το hash[9_999_999] και ούτω καθεξής…
4. Αυτός ο σχεδιασμός καθιστά αδύνατη την πλαστογράφηση μελλοντικών hash, ενώ επιτρέπει σε οποιονδήποτε να επαληθεύσει όλα τα προηγούμενα hash.
Απροβλεψιμότητα: Τα μελλοντικά αποτελέσματα παιχνιδιού δεν μπορούν να γίνουν γνωστά ή να επηρεαστούν, ακόμη και από τον διακομιστή.
Επαληθευσιμότητα: Οι παίκτες μπορούν να επαληθεύσουν την ακεραιότητα οποιουδήποτε προηγούμενου γύρου χρησιμοποιώντας μόνο το δημοσίως αποκαλυφθέν τελικό hash.
Απομόνωση Παιχνιδιού: Κάθε τύπος παιχνιδιού έχει τη δική του αλυσίδα, αποτρέποντας τη χειραγώγηση μεταξύ παιχνιδιών.
Για να διασφαλίσουμε αποδείξιμη δικαιοσύνη σε παιχνίδια ενός παίκτη, η πλατφόρμα μας χρησιμοποιεί ένα σύστημα βασισμένο σε τρία κύρια στοιχεία για τη δημιουργία του seed αποτελέσματος:
1. Client Seed: Αυτό είναι ένα seed που ελέγχεται από τον χρήστη. Δημιουργείται αυτόματα όταν ένας χρήστης εγγράφεται, αλλά ο χρήστης μπορεί να το αλλάξει ανά πάσα στιγμή. Η αλλαγή του client seed ξεκινά τη δημιουργία ενός νέου server seed.
2. Server Seed: Αυτό δημιουργείται από τον διακομιστή χρησιμοποιώντας εσωτερικές παραμέτρους που δεν αποκαλύπτονται στον χρήστη. Είναι μοναδικά συνδεδεμένο με το τρέχον client seed. Για κάθε client seed, υπάρχει ένα αντίστοιχο server seed. Όταν ο χρήστης αλλάζει το client seed, το προηγούμενο server seed αποκαλύπτεται για σκοπούς επαλήθευσης.
3. Nonce: Ένας μοναδικός αριθμός που βασίζεται στη χρονοσφραγίδα του στοιχήματος (σε χιλιοστά του δευτερολέπτου). Διασφαλίζει ότι ακόμη και επαναλαμβανόμενες ενέργειες παράγουν διαφορετικά αποτελέσματα.
Αυτές οι τρεις τιμές συνδυάζονται και κατακερματίζονται χρησιμοποιώντας HMAC-SHA256 για να δημιουργήσουν το τελικό seed παιχνιδιού, το οποίο στη συνέχεια χρησιμοποιείται για να καθορίσει το αποτέλεσμα του παιχνιδιού.
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
}
Ο διακομιστής παρέχει την κατακερματισμένη έκδοση του τρέχοντος server seed πριν πραγματοποιηθεί οποιοδήποτε στοίχημα.
Αφού ο χρήστης αλλάξει το client seed του, το προηγουμένως χρησιμοποιημένο server seed αποκαλύπτεται.
Αυτό επιτρέπει στους χρήστες να επαληθεύσουν ότι όλα τα αποτελέσματα που δημιουργήθηκαν με το προηγούμενο server seed ήταν συνεπή και δίκαια.
Στο παιχνίδι Dice, ένας χρήστης επιλέγει από 1 έως 5 αριθμούς (από τις 6 πιθανές πλευρές). Μια μεμονωμένη ρίψη ζαριού προσομοιώνεται χρησιμοποιώντας το δημιουργημένο seed παιχνιδιού. Εάν ο αριθμός που ρίχνεται ταιριάζει με μία από τις επιλογές του χρήστη, ο χρήστης κερδίζει.
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
}
Στο παιχνίδι Mines, ο στόχος είναι να αποκαλύψετε όσο το δυνατόν περισσότερα κελιά χωρίς να χτυπήσετε νάρκη. Η τοποθέτηση των ναρκών προκύπτει ντετερμινιστικά χρησιμοποιώντας ένα κρυπτογραφικά ασφαλές seed παιχνιδιού. Αυτό διασφαλίζει δικαιοσύνη και διαφάνεια, επιτρέποντας στον χρήστη να επαληθεύσει ότι ο πίνακας παιχνιδιού δεν χειραγωγήθηκε.
Είσοδοι:
seed: ένα δεκαεξαδικό αλφαριθμητικό 64 χαρακτήρων που προέρχεται από τον συνδυασμό ClientSeed, ServerSeed και Nonce χρησιμοποιώντας HMAC-SHA256.
numberOfMines: ο αριθμός των ναρκών που θα τοποθετηθούν στον πίνακα.
maxCells: συνολικός αριθμός κελιών στον πίνακα (π.χ., 25 για πλέγμα 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
}Εάν numberOfMines = 3 και maxCells = 25, η συνάρτηση θα επιστρέψει ντετερμινιστικά 3 μοναδικούς δείκτες κελιών (από 1 έως 25) όπου τοποθετούνται νάρκες.
Στο παιχνίδι Platforms, το αποτέλεσμα κάθε γύρου καθορίζεται από ένα δημόσιο, επαληθεύσιμο hash που αποτελεί μέρος μιας προηγουμένως δημοσιευμένης αλυσίδας hash.
Κάθε γύρος χρησιμοποιεί ένα hash από την αλυσίδα για να καθορίσει την «τελευταία πλατφόρμα» — την πλατφόρμα που θα εξαφανιστεί στο τέλος του παιχνιδιού.
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
}
Κατά τη διάρκεια του παιχνιδιού, οι παίκτες στέκονται σε διαφορετικές πλατφόρμες. Η πλατφόρμα που πέφτει επιλέγεται τυχαία από τις διαθέσιμες πλατφόρμες, διασφαλίζοντας ότι δεν είναι ποτέ η ίδια με την προηγουμένως πεσμένη.
Στο Rocket, το αποτέλεσμα κάθε γύρου (πολλαπλασιαστής crash) καθορίζεται χρησιμοποιώντας ένα δημόσιο hash από την προδημιουργημένη αλυσίδα hash (μοναδική ανά παιχνίδι). Το αποτέλεσμα υπολογίζεται χρησιμοποιώντας μια ντετερμινιστική συνάρτηση που μετατρέπει το hash σε πολλαπλασιαστή.
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
}
Κάθε παίκτης λαμβάνει το hash πριν από τον γύρο, και μπορεί ανεξάρτητα να επαληθεύσει ότι το αποτέλεσμα ταιριάζει με τον δημοσιευμένο τύπο, διασφαλίζοντας πλήρη διαφάνεια και αποδείξιμη δικαιοσύνη.
Στο Battle, ένας παίκτης επιλέγεται ως νικητής βάσει του αναλογικού μεγέθους του στοιχήματός του σε σχέση με τη συνολική δεξαμενή. Αυτό σημαίνει ότι μεγαλύτερα στοιχήματα οδηγούν σε υψηλότερες πιθανότητες νίκης.
Χρησιμοποιούμε έναν ντετερμινιστικό και αποδείξιμα δίκαιο μηχανισμό επιλογής:
1. Ένα δημόσιο hash παιχνιδιού χρησιμοποιείται για τη δημιουργία ενός ψευδοτυχαίου αριθμού στο εύρος [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
}