Algoritmo del sorteo · verificación pública
Cualquier persona puede reproducir el sorteo diario de Hvoy con el seed público, la instantánea de números admitidos y el número de premios.
Proceso del sorteo
Cada número comprado se registra inmediatamente y recibe un identificador global único y consecutivo, por ejemplo #00827.
El sistema congela la lista admitida, crea entries.json y publica entries_hash para que cualquier cambio posterior pueda detectarse.
El seed utiliza el último BTC tip hash cerca del momento de ejecución. Es público y Hvoy no puede controlarlo.
El hash de BTC actúa como seed para aplicar Partial Fisher-Yates Shuffle a la lista pública y elegir ganadores sin repetición.
Los ganadores reciben sus claves API y la lista pública muestra el estado de entrega.
Implementación de referencia
import crypto from "node:crypto";
export const LOTTERY_ALGORITHM_VERSION = "partial-fisher-yates-btc-hash-v1";
export function normalizeBtcBlockHash(hash) {
if (typeof hash !== "string") {
throw new TypeError("btcBlockHash must be a string");
}
let value = hash.trim().toLowerCase();
if (value.startsWith("0x")) value = value.slice(2);
if (!/^[0-9a-f]{64}$/.test(value)) {
throw new Error("btcBlockHash must be 64 hex characters");
}
return value;
}
function assertNonNegativeSafeInteger(name, value) {
if (!Number.isSafeInteger(value) || value < 0) {
throw new Error(`${name} must be a non-negative safe integer`);
}
}
function sha256(data) {
return crypto.createHash("sha256").update(data).digest();
}
export function deriveLotterySeed({ btcBlockHash, participantCount, giftCount }) {
const hash = normalizeBtcBlockHash(btcBlockHash);
assertNonNegativeSafeInteger("participantCount", participantCount);
assertNonNegativeSafeInteger("giftCount", giftCount);
const seedMaterial = [
LOTTERY_ALGORITHM_VERSION,
`btc_block_hash=${hash}`,
`participant_count=${participantCount}`,
`gift_count=${giftCount}`,
].join("\n");
return {
seed: Buffer.from(hash, "hex"),
seedHex: hash,
seedMaterial,
};
}
function bitLength(input) {
if (input < 0n) throw new Error("bitLength input must be non-negative");
let bits = 0;
let value = input;
while (value > 0n) {
bits += 1;
value >>= 1n;
}
return bits;
}
function u64be(input) {
if (input < 0n || input > 0xffffffffffffffffn) {
throw new Error("counter overflow");
}
const buffer = Buffer.alloc(8);
buffer.writeBigUInt64BE(input);
return buffer;
}
class DeterministicRng {
constructor(seed, domain) {
if (!Buffer.isBuffer(seed) || seed.length !== 32) {
throw new Error("seed must be a 32-byte Buffer");
}
this.seed = seed;
this.domain = Buffer.from(domain, "utf8");
this.counter = 0n;
this.buffer = Buffer.alloc(0);
}
randomBytes(n) {
if (!Number.isSafeInteger(n) || n < 0) {
throw new Error("n must be a non-negative safe integer");
}
while (this.buffer.length < n) {
const msg = Buffer.concat([
this.seed,
Buffer.from("|", "utf8"),
this.domain,
Buffer.from("|", "utf8"),
u64be(this.counter),
]);
const chunk = sha256(msg);
this.buffer = Buffer.concat([this.buffer, chunk]);
this.counter += 1n;
}
const out = this.buffer.subarray(0, n);
this.buffer = this.buffer.subarray(n);
return out;
}
randomIntInclusive(low, high) {
if (!Number.isSafeInteger(low) || !Number.isSafeInteger(high)) {
throw new Error("low/high must be safe integers");
}
if (low > high) throw new Error("low must be <= high");
const span = BigInt(high - low + 1);
const bits = bitLength(span - 1n);
const byteLen = Math.max(1, Math.ceil(bits / 8));
const maxValue = 1n << BigInt(byteLen * 8);
const limit = maxValue - (maxValue % span);
while (true) {
const bytes = this.randomBytes(byteLen);
const x = BigInt(`0x${bytes.toString("hex")}`);
if (x < limit) {
return low + Number(x % span);
}
}
}
}
export function drawLottery(input) {
const candidateSerials = Array.isArray(input.candidateSerials)
? input.candidateSerials.map((item) => Number(item)).filter((item) => Number.isSafeInteger(item) && item > 0)
: null;
const participantCount = candidateSerials ? candidateSerials.length : Number(input.participantCount);
const giftCount = Number(input.giftCount);
assertNonNegativeSafeInteger("participantCount", participantCount);
assertNonNegativeSafeInteger("giftCount", giftCount);
if (candidateSerials) {
const unique = new Set(candidateSerials);
if (unique.size !== candidateSerials.length) {
throw new Error("candidateSerials must be unique positive safe integers");
}
}
const serialAtPosition = (position) => candidateSerials ? candidateSerials[position - 1] : position;
const { seed, seedHex, seedMaterial } = deriveLotterySeed({
btcBlockHash: input.btcBlockHash,
participantCount,
giftCount,
});
const winnerCount = Math.min(participantCount, giftCount);
if (winnerCount === 0) {
return {
algorithm: LOTTERY_ALGORITHM_VERSION,
participantCount,
giftCount,
winnerCount,
seedHex,
seedMaterial,
drawOrderSerials: [],
winnerSerialsSorted: [],
};
}
if (giftCount >= participantCount) {
const all = Array.from({ length: participantCount }, (_, index) => serialAtPosition(index + 1));
return {
algorithm: LOTTERY_ALGORITHM_VERSION,
participantCount,
giftCount,
winnerCount,
seedHex,
seedMaterial,
drawOrderSerials: all,
winnerSerialsSorted: [...all].sort((a, b) => a - b),
};
}
const rng = new DeterministicRng(seed, "winner-serials");
const swapped = new Map();
const drawOrderSerials = [];
for (let i = 1; i <= winnerCount; i += 1) {
const j = rng.randomIntInclusive(i, participantCount);
const valueAtI = swapped.has(i) ? swapped.get(i) : i;
const valueAtJ = swapped.has(j) ? swapped.get(j) : j;
swapped.set(i, valueAtJ);
swapped.set(j, valueAtI);
drawOrderSerials.push(serialAtPosition(valueAtJ));
}
return {
algorithm: LOTTERY_ALGORITHM_VERSION,
participantCount,
giftCount,
winnerCount,
seedHex,
seedMaterial,
drawOrderSerials,
winnerSerialsSorted: [...drawOrderSerials].sort((a, b) => a - b),
};
}
export function serializeLotteryEntries(entries) {
const normalized = (entries || []).map((entry) => ({
serial_no: Number(entry.serialNo ?? entry.serial_no),
user_label: String(entry.userLabel ?? entry.user_label ?? entry.usernameLabel ?? entry.username_label ?? ""),
}));
normalized.sort((a, b) => a.serial_no - b.serial_no);
return JSON.stringify(normalized);
}
export function hashLotteryEntries(entries) {
return crypto.createHash("sha256").update(serializeLotteryEntries(entries)).digest("hex");
}Cómo verificar esta ronda
- 1. Cuando entries_hash y entries.json sean públicos, verifica primero la integridad de la instantánea.
- 2. Usa el hash de BTC como seed, lee los números candidatos de entries.json y confirma el número de premios.
- 3. Ejecuta Partial Fisher-Yates Shuffle sobre los candidatos para reproducir los ganadores.
- 4. Compara el resultado con la lista pública de ganadores.
Preguntas frecuentes
¿Por qué se usa el hash de un bloque BTC como seed?+
El BTC tip hash es público después de aparecer y Hvoy no puede elegirlo por adelantado.
¿Por qué no se publica el seed antes del sorteo?+
Solo se conoce al ejecutar la tarea, por lo que nadie puede comprar números después de ver el seed definitivo.
¿entries.json expone datos personales?+
La instantánea solo contiene números y etiquetas de usuario enmascaradas, sin correos ni teléfonos.
¿Puede cambiar el algoritmo?+
La versión pública actual se denomina Partial Fisher-Yates Shuffle. Cualquier versión futura debe anunciarse y mantener verificables los resultados históricos.