When I set out to build a provably-fair random selection system on Solana, the obvious choice for randomness was a VRF (Verifiable Random Function). Instead, I built the system around Solana's SlotHashes sysvar with a commit-reveal scheme. Here's why, and what I gave up to get there.

The problem

A fair-selection system needs a winner (or set of winners) chosen in a way that's fair, and just as important that participants can check for themselves without taking anyone's word for it. VRF services (Switchboard, ORAO, etc.) solve the fairness part well: they produce randomness that's unpredictable in advance and cryptographically provable after the fact. But they come with a dependency on an oracle, a fee per request, and a proof that most users will never actually verify they'll trust it because the crypto math says they can, not because they did.

I wanted something a participant with no crypto background could check in a browser console.

The approach: commit-reveal with slot hashes

The core idea: commit to the participant list before you know the randomness, then derive the randomness from a slot hash you couldn't have predicted at commit time.

rust
fn derive_randomness(target_hash: &[u8; 32],
participant_root: &[u8; 32]) -> [u8; 32] {
let mut combined_seed = [0u8; 64];
combined_seed[..32].copy_from_slice(target_hash); // slot hash at reveal
combined_seed[32..].copy_from_slice(participant_root); // Merkle root, locked at commit
solana_keccak_hasher::hash(&combined_seed).to_bytes()
}

The flow:

Commit: participant list is finalized and hashed into a Merkle root; this is written on-chain.
Wait: a target slot in the future is chosen as the reveal point.
Reveal: once that slot passes, its hash is pulled from SlotHashes and combined with the committed root to derive the randomness.
Select: the randomness deterministically picks winners from the participant set; winners get their own Merkle root and proofs.

Every draw ends up with an audit record like:

rust
pub struct AuditResult {
pub draw_id: String,
pub merkle_root: String, // participants commitment
pub commit_slot: u64, // when the list was locked
pub reveal_slot: u64, // when randomness was revealed
pub randomness: String, // derived randomness
pub winners: Vec, // selected winners
pub winner_root: String, // winners commitment
}

Anyone can independently redo the whole thing: pull the slot hash from the chain, recompute the keccak mix, rerun selection, and check their own inclusion/win with a Merkle proof no SDK, no oracle client, no trust in a third party's proof format.

What this actually trades off

I want to be more careful here than the "VRF vs. slot hash" framing usually is, because it's not simply "same guarantee, less friction."

Where slot hashes are weaker: a slot's leader has some influence over what gets included in a block, and in theory a validator who is also a participant (or colludes with one) could, in narrow cases, choose whether to produce a slot at all, which nudges which hash ends up being the reveal target. VRF is specifically designed to remove that class of manipulation the output is unpredictable and unbiasable even by the party running it, and that's a genuinely stronger cryptographic guarantee. For a high-value single draw, that matters.

Where slot hashes are stronger, practically: verification. VRF proofs are publicly checkable in principle, but checking one means pulling in the oracle's SDK, the right public key, and understanding the underlying proof system. In practice almost no end user does this they trust the label "verifiable," not the verification itself. A slot hash and a keccak mix, by contrast, is auditable with a block explorer and a calculator.

Where slot hashes win outright: cost and latency. No oracle fee, no waiting on an external service, and the reveal is just "has this slot passed yet."

So the honest version of the tradeoff is: VRF gives you a stronger guarantee against a specific (narrow, costly-to-execute) manipulation vector, in exchange for a dependency, a fee, and verification that in practice only crypto tooling actually exercises. Slot hashes give up some of that manipulation resistance in exchange for zero-dependency verification that a participant can actually do themselves and the manipulation surface can be narrowed further by using a commit slot far enough in advance and a reveal slot the committer can't foresee at commit time.

For a system where the stakes per draw are modest, that's a reasonable trade. It wouldn't be my choice for something where a validator has a large enough incentive to attempt slot-level manipulation a high-value on-chain lottery, say.

Takeaway

"Provably fair" is thrown around a lot in this space, often to mean "there's a cryptographic proof somewhere." I think the more useful bar is: can the person who actually needs to trust the result do the verification themselves? For this platform, slot hashes plus Merkle proofs cleared that bar in a way a VRF integration wouldn't have, at the cost of a narrower and well-understood weakness rather than a hidden one.

Code's on GitHub if you want to dig into the commit-reveal implementation or the Merkle proof structure directly.

What's your take, where's the line for you between "cryptographically strongest" and "verifiable by a normal person"?