Los nombres de los premios y las etiquetas públicas de usuario se muestran en el idioma en que se configuraron o enviaron.
Estado del sorteo actual
-
Se rige por el horario configurado para esta ronda

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.

Algoritmo
Barajado parcial de Fisher-Yates
v1.0
Origen del seed
Hash del último bloque BTC
Se rige por el horario configurado para esta ronda
Selección sin repetición
Selección sin repetición
Un número no puede ganar dos veces

Proceso del sorteo

1
Participación · 18:00 – 12:00

Cada número comprado se registra inmediatamente y recibe un identificador global único y consecutivo, por ejemplo #00827.

2
Cierre · 12:00

El sistema congela la lista admitida, crea entries.json y publica entries_hash para que cualquier cambio posterior pueda detectarse.

3
Espera del seed · 12:00 – 14:00

El seed utiliza el último BTC tip hash cerca del momento de ejecución. Es público y Hvoy no puede controlarlo.

4
Sorteo · 14:00

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.

5
Entrega · después de las 14:00

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. 1. Cuando entries_hash y entries.json sean públicos, verifica primero la integridad de la instantánea.
  2. 2. Usa el hash de BTC como seed, lee los números candidatos de entries.json y confirma el número de premios.
  3. 3. Ejecuta Partial Fisher-Yates Shuffle sobre los candidatos para reproducir los ganadores.
  4. 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.