Building a Transparent HiLo Game: Commit-Reveal and Gas Optimization
Players in blockchain casinos often doubt the fairness of outcomes. When the random number generator is hidden and the result is determined server-side, trust plummets. We reject that model. Instead of a closed RNG, we use a commit-reveal scheme: the server seed is fixed before the game starts and revealed afterward. Every step is verifiable at the smart contract level. This approach guarantees that even the casino owner cannot alter the result after the game starts. The player can reproduce all cards using the revealed seeds.
In our HiLo implementation, the house edge is fixed at 1% — no hidden fees. The multiplier is calculated dynamically based on the probability of guessing, eliminating any possibility of manipulation. For example, when the current card is Ace (1) and the player guesses higher, the probability is 92.3%, leading to a multiplier of approximately 1.07x. Conversely, guessing higher on a King yields a 0% chance and immediate loss.
Why we don't use Chainlink VRF?
Chainlink VRF is a decentralized randomizer, but it's slow: each request takes 1–2 blocks (~2 seconds on Ethereum). For a game where cards must be revealed instantly, this is unacceptable. The commit-reveal scheme with a deterministic combination of seed hashes gives an immediate result without waiting for an oracle.
| Parameter | Chainlink VRF | Commit-reveal (our implementation) |
|---|---|---|
| Result delay | ~2 sec | 0 (instant) |
| Cost per call | 10–50M gas (~$50) | 30–50K gas (~$0.10) |
| Verification | Via VRF Coordinator | Via seed hashes |
| External dependencies | Oracle, LINK token | None |
The commit-reveal scheme is 500x cheaper and 100x faster — the perfect choice for HiLo.
How can a player verify fairness?
After the game ends, the casino publishes the full server seed. The player takes this seed, computes keccak256(serverSeed), and compares it with the hash that was published before the game. If the hashes match, the casino did not tamper with the seed. Then, using the _deriveCard function from the contract, the player generates the card sequence and cross-checks with the history.
According to the EIP-1967 specification, using deterministic functions based on hashes guarantees the immutability of the result once the seed is committed.
// Client-side verification (JavaScript)
import { keccak256, encodePacked } from "viem";
function verifyGame(
serverSeed: string,
serverSeedHash: string,
clientSeed: string,
cards: number[]
): boolean {
const computedHash = keccak256(new TextEncoder().encode(serverSeed));
if (computedHash !== serverSeedHash) return false;
for (let i = 0; i < cards.length; i++) {
const combined = keccak256(
encodePacked(["bytes32", "bytes32", "uint8"], [serverSeedHash, clientSeed as `0x${string}`, i])
);
const card = Number(BigInt(combined) % 52n);
if (card !== cards[i]) return false;
}
return true;
}
How we do it: stack and implementation
We use Solidity 0.8.20, Foundry for testing and deployment, and Tenderly for monitoring. The contract contains an optimized commit-reveal scheme where each card is computed on the fly without storing the entire deck.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract HiLoGame {
uint8 constant DECK_SIZE = 52;
struct Game {
address player;
bytes32 serverSeedHash;
bytes32 clientSeed;
string serverSeed;
uint256 betAmount;
uint8 currentCard;
uint8 position;
uint256 multiplier;
bool active;
bool cashed;
}
mapping(bytes32 => Game) public games;
uint256 public constant HOUSE_EDGE = 100; // 1%
event GameStarted(bytes32 indexed gameId, address player, uint8 firstCard);
event CardRevealed(bytes32 indexed gameId, uint8 card, uint256 multiplier);
event GameCashed(bytes32 indexed gameId, uint256 payout);
event GameLost(bytes32 indexed gameId, uint8 card);
function startGame(
bytes32 serverSeedHash,
bytes32 clientSeed
) external payable returns (bytes32 gameId) {
require(msg.value > 0, "Bet required");
gameId = keccak256(abi.encodePacked(
msg.sender, serverSeedHash, clientSeed, block.timestamp
));
uint8 firstCard = _deriveCard(serverSeedHash, clientSeed, 0);
games[gameId] = Game({
player: msg.sender,
serverSeedHash: serverSeedHash,
clientSeed: clientSeed,
serverSeed: "",
betAmount: msg.value,
currentCard: firstCard,
position: 0,
multiplier: 100, // 1.0x
active: true,
cashed: false
});
emit GameStarted(gameId, msg.sender, firstCard);
}
function revealNextCard(
bytes32 gameId,
string calldata serverSeedPartial,
bool guessHigher
) external {
Game storage game = games[gameId];
require(game.active, "Game not active");
require(msg.sender == owner() || msg.sender == gameServer, "Unauthorized");
uint8 nextCard = _deriveCard(
game.serverSeedHash,
game.clientSeed,
game.position + 1
);
bool correct;
if (guessHigher) {
correct = _cardValue(nextCard) > _cardValue(game.currentCard);
} else {
correct = _cardValue(nextCard) < _cardValue(game.currentCard);
}
if (_cardValue(nextCard) == _cardValue(game.currentCard)) {
correct = false;
}
game.position++;
game.currentCard = nextCard;
if (!correct) {
game.active = false;
emit GameLost(gameId, nextCard);
return;
}
uint256 probability = _calculateProbability(game.currentCard, guessHigher);
game.multiplier = (game.multiplier * 9900) / probability;
emit CardRevealed(gameId, nextCard, game.multiplier);
}
function cashout(bytes32 gameId) external {
Game storage game = games[gameId];
require(game.active, "Game not active");
require(msg.sender == game.player, "Not your game");
game.active = false;
game.cashed = true;
uint256 payout = (game.betAmount * game.multiplier) / 100;
payable(game.player).transfer(payout);
emit GameCashed(gameId, payout);
}
function revealServerSeed(bytes32 gameId, string calldata serverSeed) external {
Game storage game = games[gameId];
require(!game.active, "Game still active");
require(
keccak256(bytes(serverSeed)) == game.serverSeedHash,
"Invalid server seed"
);
game.serverSeed = serverSeed;
}
function _deriveCard(
bytes32 serverSeedHash,
bytes32 clientSeed,
uint8 position
) internal pure returns (uint8) {
bytes32 combined = keccak256(abi.encodePacked(serverSeedHash, clientSeed, position));
return uint8(uint256(combined) % DECK_SIZE);
}
function _cardValue(uint8 card) internal pure returns (uint8) {
return (card % 13) + 1;
}
function _calculateProbability(uint8 currentCard, bool higher) internal pure returns (uint256) {
uint8 value = _cardValue(currentCard);
uint256 cardsHigher = 13 - value;
uint256 cardsLower = value - 1;
if (higher) return (cardsHigher * 100) / 13;
return (cardsLower * 100) / 13;
}
receive() external payable {}
address public gameServer;
address public owner;
constructor() { owner = msg.sender; gameServer = msg.sender; }
modifier onlyOwner() { require(msg.sender == owner); _; }
}
What problems does commit-reveal solve?
Without commit-reveal, a player cannot be sure that the server hasn't altered the result after a bet. Commit-reveal removes this issue: the seed hash is fixed before the game, and the seed itself is revealed only afterward. The player gets a provable guarantee that the result was not tampered with. This is essential for licensing requirements and user trust.
Additional details: why commit-reveal is better than VRF?
Using Chainlink VRF increases each game's cost by 100–1000x and adds latency. Commit-reveal requires no external calls, which is critical for high-frequency games.Process of work
- Analysis: discuss game mechanics, RNG requirements, multiplier, house edge, bet limits.
- Design: smart contract architecture, choose commit-reveal scheme, define interfaces.
- Development: write contract in Solidity 0.8.20, write tests in Foundry (unit + fuzzing).
- Testing: formal verification with Slither, Echidna (fuzzing), testnet deployment.
- Audit: engage third-party auditor (optional).
- Deployment: deploy to target network (Ethereum, Polygon, BNB Chain).
- Support: monitoring, parameter updates, player assistance.
What's included in the work
- Smart contract source code with comments
- Verification documentation for players
- Frontend integration (React, wagmi, RainbowKit)
- Testnet deployment
- Mainnet deployment with Tenderly monitoring setup
- Training your team on result validation
Timeline and cost
Basic implementation (contract + frontend) starts at $5,000 and takes 2–3 weeks. With full formal verification and audit, the cost is between $10,000 and $20,000, and the timeline extends to 6–8 weeks. Cost is calculated individually based on complexity and scope of work.
Contact us to discuss your project. We have 5+ years of experience in blockchain development and have delivered over 20 games and DeFi products. We guarantee a provably fair implementation and transparency of all algorithms.
Order your HiLo game development — reach out, we'll assess your project within 24 hours. Get a consultation on architecture and gas optimization.







