node-provably-fair
Version:
A provably fair system for generating and verifying random numbers. Includes support for generating random outcomes based on blockchain data, ensuring fair and tamper-proof results.
192 lines (164 loc) • 7.37 kB
text/typescript
import axios from 'axios';
import * as crypto from 'crypto';
import * as os from 'os';
import { v4 as uuidv4 } from 'uuid';
export class ProvablyFair {
private serverSeed: string;
private secretSalt: string;
private publicHash: string;
private nonce: number;
constructor() {
this.serverSeed = this.generateSecureRandomString(64);
this.secretSalt = this.generateSecureRandomString(8);
this.nonce = 0;
this.publicHash = this.generatePublicHash(this.serverSeed, this.secretSalt);
}
private generateSecureRandomString(length: number): string {
return crypto.randomBytes(length).toString('hex');
}
private generatePublicHash(serverSeed: string, secretSalt: string): string {
return crypto.createHash('sha256').update(serverSeed + secretSalt).digest('hex');
}
private generateRollHash(clientSeed: string, serverSeed: string, secretSalt: string, nonce: number, bitcoinHash: string, max: number = 10000000): string {
const combinedInput = `${bitcoinHash}:${clientSeed}:${serverSeed}:${nonce}`;
const randomNumber = crypto.createHmac('sha256', secretSalt).update(combinedInput).digest('hex');
const decimalValue = parseInt(randomNumber.slice(0, 8), 16);
return ((decimalValue % max) + 1).toString();
}
public verifyRoll(
clientSeed: string,
roll: number,
serverSeed: string,
secretSalt: string,
nonce: number,
publicHash: string,
bitcoinHash: string
): boolean {
const generatedPublicHash = this.generatePublicHash(serverSeed, secretSalt);
if (generatedPublicHash !== publicHash) {
return false;
}
const generatedRoll = this.generateRollHash(clientSeed, serverSeed, secretSalt, nonce, bitcoinHash);
return generatedRoll === roll.toString();
}
public calculateWinProbability(winRange: number, totalRange: number = 10000000): number {
if (winRange < 1 || winRange > totalRange) {
throw new Error('The winning range must be between 1 and the total possibilities.');
}
return (winRange / totalRange) * 100;
}
public getPublicHash(): string {
return this.publicHash;
}
public getNonce(): number {
return this.nonce;
}
public incrementNonce(): void {
this.nonce += 1;
}
public rotateSeeds(): void {
this.serverSeed = this.generateSecureRandomString(64);
this.secretSalt = this.generateSecureRandomString(8);
this.publicHash = this.generatePublicHash(this.serverSeed, this.secretSalt);
this.nonce = 0; // Reset nonce on seed rotation
}
/**
* Returns a win interval based on the provided percentage.
* @param percentage - The desired win percentage.
* @param totalRange - The total possibilities (usually 10,000,000).
* @returns The win interval corresponding to the percentage.
*/
public getWinInterval(percentage: number, totalRange: number = 10000000): { percentage: number, interval: [number, number] } {
const winRange = Math.round((percentage / 100) * totalRange); // calculate the size of the winning range based on the percentage
const end = totalRange; // The end will always be the totalRange
const start = totalRange - winRange + 1; // Calculate the start to ensure the range ends at the totalRange
return {
percentage,
interval: [start, end]
};
}
/**
* Returns an array of intervals based on a provided array of percentages.
* @param percentages - An array of percentages.
* @param totalRange - The total possibilities (usually 10,000,000).
* @returns An array of objects containing percentage and corresponding interval.
*/
public getWinIntervals(percentages: number[], totalRange: number = 10000000): { percentage: number, interval: [number, number] }[] {
let currentStart = 1;
return percentages.map((percentage) => {
const winRange = Math.round((percentage / 100) * totalRange);
const end = currentStart + winRange - 1;
const interval: [number, number] = [currentStart, end];
currentStart = end + 1;
return { percentage, interval };
});
}
/**
* Generates a random number based on the hash of the latest Bitcoin blockchain block.
* @param clientSeed - The client seed.
* @param serverSeed - The server seed (optional, generated if not provided).
* @param secretSalt - The secret salt (optional, generated if not provided).
* @param max - The desired maximum range.
* @returns An object containing the generated number, serverSeed, secretSalt, nonce, clientSeed, and publicHash used.
*/
public async generateProvably(
clientSeed: string,
serverSeed?: string,
secretSalt?: string,
max: number = 10000000
): Promise<{ randomNumber: number, serverSeed: string, secretSalt: string, nonce: number, clientSeed: string, publicHash: string, bitcoinHash: string }> {
const bitcoinHash = await this.getLatestBitcoinBlockHash();
const nonce = this.generateRandomNonce(); // Generating random nonce
// Use provided values or generate new ones
const finalServerSeed = serverSeed || this.generateSecureRandomString(64);
const finalSecretSalt = secretSalt || this.generateSecureRandomString(8);
// Generating public hash
const publicHash = this.generatePublicHash(finalServerSeed, finalSecretSalt);
const combinedInput = `${bitcoinHash}:${clientSeed}:${finalServerSeed}:${nonce}`;
const randomNumber = crypto.createHmac('sha256', finalSecretSalt).update(combinedInput).digest('hex');
const decimalValue = parseInt(randomNumber.slice(0, 8), 16);
return {
randomNumber: (decimalValue % max) + 1,
serverSeed: finalServerSeed,
secretSalt: finalSecretSalt,
nonce,
clientSeed,
publicHash,
bitcoinHash
};
}
/**
* Generates a random nonce with extra entropy for maximum randomness.
* @returns A random integer.
*/
private generateRandomNonce(): number {
const timeComponent = Date.now(); // Current time
// Get additional entropy from the system
const systemEntropy = [
process.pid, // Process ID
os.uptime(), // System uptime
os.freemem(), // Free memory
os.loadavg()[0] // System load average
].join('-');
const randomBuffer1 = crypto.randomBytes(8); // Generate 8 bytes of additional entropy
const randomBuffer2 = crypto.randomBytes(8);
const randomValue1 = randomBuffer1.readUInt32BE(0);
const randomValue2 = randomBuffer2.readUInt32BE(0);
// Generate a UUID v4 to add more entropy
const uuid = uuidv4();
// Combine all entropy sources
const combinedValue = `${timeComponent}-${systemEntropy}-${randomValue1}-${randomValue2}-${uuid}`;
// Apply a final hash to ensure uniformity
const hash = crypto.createHash('sha256').update(combinedValue).digest('hex');
// Convert to a number
return parseInt(hash.slice(0, 8), 16);
}
/**
* Retrieves the latest Bitcoin blockchain block hash using Blockchain.info API.
* @returns The latest block hash.
*/
private async getLatestBitcoinBlockHash(): Promise<string> {
const response = await axios.get('https://blockchain.info/latestblock');
return response.data.hash;
}
}