turnley.dev

PHP and MySQL code for faucet operators

Tools & Calculators

ENG-002

A provably fair draw anyone can verify

Commit to a server seed before the draw, publish it after, and let any user reproduce the result themselves. The scheme, and the mistake that makes it worthless.

Every lottery on a faucet is accused of being rigged, and without a verification scheme there is no way to answer. Provable fairness makes the accusation testable instead of a matter of trust.

The scheme: generate a server seed, publish its hash before the draw, mix it with a client seed the user controls and a nonce, and derive the outcome from the HMAC. Publish the server seed afterwards so anyone can recompute it. Because the hash was published first, you could not have changed the seed once bets were placed.

The mistake that voids the whole thing is reusing a server seed after revealing it. Once it is public, every future outcome using it is predictable in advance by the user.

PHP
function create_round(PDO $pdo): int
{
    $serverSeed = bin2hex(random_bytes(32));
    $commitment = hash('sha256', $serverSeed);      // published NOW

    $pdo->prepare(
        'INSERT INTO rounds (server_seed, commitment, status, created_at)
         VALUES (?, ?, "open", NOW())'
    )->execute([$serverSeed, $commitment]);

    return (int) $pdo->lastInsertId();              // show the commitment, never the seed
}

function draw_result(string $serverSeed, string $clientSeed, int $nonce, int $range): int
{
    $message = $clientSeed . ':' . $nonce;
    $hash = hash_hmac('sha256', $message, $serverSeed);

    // Take 52 bits — comfortably inside the exact-integer range of a double,
    // and far more entropy than any prize table needs.
    $slice = substr($hash, 0, 13);
    $value = hexdec($slice);

    return (int) ($value % $range);
}

function settle_round(PDO $pdo, int $roundId, int $range): array
{
    $st = $pdo->prepare('SELECT * FROM rounds WHERE id = ? AND status = "open" FOR UPDATE');
    $st->execute([$roundId]);
    $round = $st->fetch(PDO::FETCH_ASSOC);
    if (!$round) {
        throw new RuntimeException('round not open');
    }

    $winner = draw_result($round['server_seed'], (string) $round['client_seed'], (int) $round['nonce'], $range);

    // Reveal the seed and retire it. A revealed seed is NEVER reused: every
    // future outcome from it would be predictable by anyone who saw it.
    $pdo->prepare('UPDATE rounds SET status = "settled", winner = ?, settled_at = NOW() WHERE id = ?')
        ->execute([$winner, $roundId]);

    return ['winner' => $winner, 'server_seed' => $round['server_seed'], 'commitment' => $round['commitment']];
}

Using it

Publish the commitment on the round page before entries close, and the seed plus a worked example after. A verification page where a user pastes the three values and gets the same answer is what makes it real.

Let users set their own client seed. Without user-controlled input, you could grind server seeds until you liked the outcome.

Keep every settled round visible with its seed and commitment. The history is the proof.

What bites people

Never reuse a revealed server seed. One round, one seed.

random_bytes, not mt_rand. A predictable seed defeats the commitment entirely.

Modulo introduces a small bias when the range does not divide the space evenly. Irrelevant at 52 bits and a small prize table; not irrelevant if you ever narrow the hash slice.

This one touches real money. Point it at a throwaway wallet and watch a full cycle before you trust it with a live balance.

Also in Engagement and Content