Commit-Reveal Schemes: Pattern, When to Use It, and Common Implementation Bugs (Complete Guide)

Commit-Reveal Schemes: Pattern, When to Use It, and Common Implementation Bugs (Complete Guide)

Commit-Reveal Schemes are one of the cleanest ways to reduce front running, limit manipulation, and make on-chain choices fairer when users must submit secrets. But the pattern is easy to implement badly. This guide explains when commit-reveal is the right tool, how it works in practice, and the most common bugs that quietly break the guarantees. You will get a safety-first workflow, concrete Solidity examples, and a set of checks you can reuse before deploying.

TL;DR

  • Commit-Reveal Schemes hide user intent in phase one (commit), then prove it in phase two (reveal) so others cannot copy, censor, or front run based on the original choice.
  • Commit-reveal is not a magic anti-MEV shield. It reduces certain classes of attacks, but it adds new failure modes: griefing, non-reveal, timing games, and weak commitments.
  • The core rule: a commit must bind a user to one choice, and a reveal must prove that choice without letting others steal it.
  • Most real-world bugs come from weak hashing inputs, missing domain separation, broken deadlines, missing nonces, and incorrect state transitions.
  • Prerequisite reading for baseline Solidity threat modeling: Solidity Security Basics.
  • For structured learning and deeper protocol design: Blockchain Technology Guides and Blockchain Advance Guides.
  • To sanity-check deployed token contracts and common admin-risk patterns while you research: Token Safety Checker.
  • For ongoing smart contract security playbooks and pattern breakdowns: Subscribe.
Safety-first The pattern is simple, the guarantees are not

Commit-reveal is often described as "hash now, reveal later". That description is correct, but incomplete. The real question is: what exactly is the commitment binding to, what are the allowed timings, and what happens if someone refuses to reveal. Your design must explicitly handle non-reveal, late reveal, replay, and copied reveals.

If you have not built a mental model of common Solidity failure modes, start with Solidity Security Basics.

A clean mental model that matches production reality

A commit-reveal scheme is a two phase protocol for choices that should be private until a later moment. In phase one, a user publishes a commitment that proves they chose something, but does not reveal the choice. In phase two, the user reveals the choice and provides whatever secrets are required so the contract can verify the commitment matches.

The simplest analogy is sealing a vote in an envelope. The envelope is submitted first so nobody can change their vote after seeing others. The envelope is opened later to count the vote. The envelope must be tamper resistant, and it must be clear who submitted it. On-chain, the envelope is usually a hash, and the tamper resistance depends on cryptographic preimage resistance and correct domain separation.

This matters because many on-chain environments are adversarial by default. Validators and block builders can reorder transactions. Searchers can copy calldata. Competitors can watch pending transactions. If your protocol depends on secret intent staying secret, you either need privacy infrastructure or a pattern like commit-reveal to avoid trivial copying.

Where commit-reveal shines

  • Auctions: sealed bids so bidders cannot outbid at the last second by reading mempool bids.
  • Randomness inputs: users contribute entropy without letting others adapt to the raw entropy.
  • Voting with delayed disclosure: prevent early votes from influencing later votes.
  • Fair mint lists and allowlists: reduce copycat claims where a bot steals the intent.
  • Game moves: prevent an opponent from reacting to your move before you can commit to it.

Where commit-reveal is often the wrong tool

Commit-reveal adds latency and operational complexity. It creates a second transaction requirement for users, and that alone can break UX and participation. It also creates non-reveal griefing risks. If your application cannot tolerate users failing to reveal, you must design penalties and timeouts.

In some cases, the right solution is not commit-reveal but a different architecture: off-chain matching with on-chain settlement, a trusted oracle, a verifiable randomness source, or a privacy preserving rollup environment. Commit-reveal is best used when you need a simple, auditable mechanism that works today and you can accept the extra phase.

Commit-reveal as a production protocol The contract must bind identity, choice, timing, and penalties for non-reveal. Phase 1: Commit User submits hash(commitData) that hides choice Contract stores commitHash and commitBlockTime Waiting window Commit window closes, reveal window opens Late commits rejected, early reveals rejected Phase 2: Reveal User submits choice + salt (and any binding fields) Contract recomputes hash, verifies it matches stored commit Failure handling If user never reveals, protocol must still progress Apply penalty, ignore commit, or allow challenge and timeout

How the pattern works under the hood

Most commit-reveal implementations use a commitment hash like keccak256(abi.encode(...)). The commitment hash must have two properties. First, it must be binding, meaning the committer cannot later reveal a different choice that still matches the same commitment. Second, it must be hiding, meaning observers cannot infer the choice from the commitment before reveal.

Binding is achieved by hashing enough information so the choice is uniquely determined. Hiding is achieved by including a high entropy secret, typically called a salt or nonce. Without a salt, many choices are brute forceable. If choices are small, like a vote among five options, an attacker can hash each option and match the commitment. With a salt, brute forcing becomes computationally infeasible if the salt is truly random and large enough.

What should be inside a commitment

The right commitment content depends on what you are protecting. A safe default is to include:

  • The choice: bid amount, vote, random seed, move, or claim.
  • A secret salt: 32 bytes from a secure RNG, generated client side.
  • The user identity binding: often msg.sender, sometimes a delegated identity or EIP-712 signer.
  • The contract domain: contract address and chain id, to prevent cross-contract or cross-chain replay.
  • The round identifier: auction id, epoch id, proposal id, or game id.

Developers frequently skip domain fields because the commitment still appears to verify. The bug shows up later as replay or cross-instance collisions.

Timing is part of the security model

A commit-reveal protocol has at least two timings: a commit deadline and a reveal deadline. These deadlines are not just UI details. They define what strategies are possible for attackers. If reveals are allowed too early, a user can reveal instantly and others can adapt. If commits are allowed too late, someone can watch reveals and then commit accordingly. Your state machine must enforce the phases strictly.

The non-reveal problem that most teams ignore

In an ideal world, everyone reveals. In the real world, some users fail to reveal because of UX friction, loss of keys, or deliberate strategy. If your protocol can be stalled by non-reveal, you have created a griefing vector. A strong design defines what happens when a reveal is missing and ensures the system progresses anyway.

Typical approaches:

  • Ignore unrevealed commits: safe for some voting and games, but can bias outcomes.
  • Require a deposit: burned or redistributed if the user fails to reveal, discouraging griefing.
  • Challenge period: allow others to finalize after reveal deadline, locking in the outcome.
  • Default action: treat non-reveal as abstain, or as a neutral move, depending on the domain.

When to use commit-reveal, and when not to

The safest way to decide is to start from the threat model. Ask: what exact attack are you preventing, and what new attacks are you willing to accept. Commit-reveal primarily helps when the main risk is information leakage before finalization. It does not solve everything, and it can be worse than doing nothing if implemented carelessly.

Decision table

Use case What you want to prevent Commit-reveal helps? Hidden cost Often better alternative
Sealed-bid auctions Bid sniping and mempool copying Yes, strongly Non-reveal griefing, two tx UX Off-chain order flow with on-chain settlement (if trust acceptable)
Randomness contributions Adaptive entropy choices Yes, partially Last-revealer advantage VRF, beacon, or threshold randomness
Voting Bandwagon effects, bribery signals Sometimes Complexity, abstain manipulation Off-chain commit, on-chain tally with proof, or privacy tech
Game moves Opponent reacting to move Yes Timeout handling Turn-based off-chain with dispute resolution
Fair allowlists Copycat claims Sometimes Extra step blocks users Signature based claims (if secrecy not required)
Anti-MEV for swaps Sandwich attacks Rarely Latency is huge Private order flow, RFQ, or intent based systems

A key warning about expectations

Commit-reveal hides intent from the public until reveal, but it does not hide it forever. If your threat is "nobody should ever know what I chose", then commit-reveal is the wrong tool. It is a fairness tool, not a privacy tool. Also, if your threat is "validators will censor my reveal", you must design force inclusion strategies and generous reveal windows.

Common implementation bugs that break the guarantees

Most commit-reveal failures are not cryptography failures. They are engineering failures. The scheme depends on a correct binding between phases, correct domain separation, correct timing, and correct state transitions. Bugs often show up as stolen reveals, replayed commits, broken deadlines, or commitments that can be brute forced.

Bug 1: weak salt or no salt

If the commitment is keccak256(choice) or even keccak256(choice, user) with no salt, it is often brute forceable. Many choices are low entropy: votes, small ranges, or IDs. An attacker computes all possible hashes and learns the choice before reveal.

Fix: require a 32 byte salt and recommend generating it with a secure RNG. Also, do not store salts on-chain. The salt must remain private until reveal.

Bug 2: incorrect encoding, collisions, or type ambiguity

Using abi.encodePacked without careful type boundaries can create collisions. Collisions can allow two different input sets to produce the same bytes and therefore the same commitment. That breaks the binding property and can allow a user to reveal a different choice.

Fix: prefer abi.encode for structured encoding, or use EIP-712 typed data signing and hash the typed data. If you must use packed encoding, separate fields with clear delimiters and fixed sizes.

Bug 3: missing domain separation

If the commitment does not include the contract address and chain id, the same commitment could be replayed on a fork, testnet, or a cloned contract with identical logic. This becomes a real risk when commitment values are signed or reused across deployments.

Fix: include address(this) and block.chainid in the commitment, and include a round id so commitments cannot be moved between rounds.

Bug 4: not binding the committer identity

If anyone can reveal a commitment without proving they are the original committer, a watcher can steal the reveal. This is the classic copied-calldata problem: the attacker watches the reveal in the mempool, copies the parameters, and submits first. If the reveal function credits msg.sender, the attacker wins.

Fix: bind commitments to the committer. Store the committer address at commit time, and require that msg.sender matches during reveal. Alternatively, bind reveal rights to a signature of the committer, but be careful not to reintroduce replay risks.

Bug 5: broken timing and phase gating

Common failures:

  • Allowing reveal before commit window closes, enabling adaptive commits.
  • Allowing commit after reveal window opens, enabling reaction commits.
  • Using block.timestamp but not accounting for miner manipulation within allowed drift.
  • Miscomputing deadlines during upgrades or epoch changes, trapping user funds.

Fix: implement a clear state machine. Use explicit phase checks. Keep windows long enough for real users. If using timestamp, treat it as approximate and do not use it for microsecond precision logic.

Bug 6: non-reveal griefing and outcome manipulation

If a user can commit and then choose whether to reveal based on what others reveal, they gain an option. That option can be used to bias outcomes. In randomness protocols, this is the last-revealer advantage. In auctions, bidders can commit then refuse to reveal if losing, wasting others' time. In voting, participants can withhold to break quorum or bias a threshold.

Fix: require a deposit, define penalties, and design finalization so the system progresses regardless of missing reveals. Sometimes the best fix is to avoid commit-reveal and use a different primitive for randomness.

Bug 7: storage mistakes and replay within the same contract

If commitments are not deleted or marked as consumed after reveal, a user may be able to reveal again, or an attacker may replay reveal parameters. If commitments are keyed incorrectly, one user can overwrite another user's commitment.

Fix: store commitments in mappings keyed by (roundId, committer) and include per-user nonces if users can commit multiple times. Mark a commitment as revealed and do not allow reuse.

Bug 8: commit front running and commitment theft

A subtle attack: the commitment itself can be stolen if it is not bound to msg.sender. A bot can see your commit hash in the mempool and submit the same commit hash first, becoming the owner of that commitment. Then your reveal fails because the contract thinks you were not the committer.

Fix: bind the commitment to the committer address inside the hashed data, and also store committer at commit time. That makes commitment theft useless because the attacker cannot reveal without the original salt and cannot claim rewards if msg.sender must match.

A secure baseline implementation in Solidity

The code below is a baseline that you can adapt. It is not a full production auction or voting system. It is a reference implementation of the commit-reveal core that addresses the most common failure modes: identity binding, domain separation, structured encoding, phase gating, replay prevention, and non-reveal finalization.

Solidity example: commit-reveal core with phases and penalties
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/// @notice Minimal commit-reveal framework.
/// @dev This is a template. You must adapt the "choice" type and outcome logic.
contract CommitRevealCore {
    enum Phase { Commit, Reveal, Finalized }

    struct CommitInfo {
        bytes32 commitHash;
        uint64 commitTime;
        bool revealed;
    }

    Phase public phase;
    uint64 public commitDeadline;
    uint64 public revealDeadline;

    uint256 public constant MIN_DEPOSIT = 0.01 ether;
    mapping(address => CommitInfo) public commits;
    mapping(address => uint256) public deposits;

    event Committed(address indexed user, bytes32 indexed commitHash);
    event Revealed(address indexed user, bytes32 choiceHash);
    event Finalized(uint256 totalReveals, uint256 totalNonReveals);

    constructor(uint64 commitWindowSeconds, uint64 revealWindowSeconds) {
        require(commitWindowSeconds > 0 && revealWindowSeconds > 0, "bad windows");
        phase = Phase.Commit;
        commitDeadline = uint64(block.timestamp) + commitWindowSeconds;
        revealDeadline = commitDeadline + revealWindowSeconds;
    }

    /// @notice Compute the commitment for a user.
    /// @dev Include domain separation, user binding, and a round identifier if applicable.
    function computeCommitment(
        address user,
        bytes32 choiceHash,
        bytes32 salt
    ) public view returns (bytes32) {
        // Domain separation: chain + contract.
        // User binding: msg.sender identity.
        // choiceHash: hashed representation of choice (so variable-sized input is normalized).
        return keccak256(abi.encode(
            block.chainid,
            address(this),
            user,
            choiceHash,
            salt
        ));
    }

    /// @notice Commit by posting a commitment hash plus a deposit to discourage non-reveal griefing.
    function commit(bytes32 commitHash) external payable {
        require(phase == Phase.Commit, "not commit phase");
        require(block.timestamp <= commitDeadline, "commit closed");
        require(msg.value >= MIN_DEPOSIT, "deposit too small");
        require(commits[msg.sender].commitHash == bytes32(0), "already committed");

        commits[msg.sender] = CommitInfo({
            commitHash: commitHash,
            commitTime: uint64(block.timestamp),
            revealed: false
        });
        deposits[msg.sender] = msg.value;

        emit Committed(msg.sender, commitHash);
    }

    /// @notice Move to reveal phase after commit deadline.
    function startRevealPhase() external {
        require(phase == Phase.Commit, "wrong phase");
        require(block.timestamp > commitDeadline, "commit still open");
        phase = Phase.Reveal;
    }

    /// @notice Reveal the choice by providing the original salt and normalized choiceHash.
    /// @dev You should also include a "roundId" in computeCommitment if the contract runs multiple rounds.
    function reveal(bytes32 choiceHash, bytes32 salt) external {
        require(phase == Phase.Reveal, "not reveal phase");
        require(block.timestamp <= revealDeadline, "reveal closed");

        CommitInfo storage info = commits[msg.sender];
        require(info.commitHash != bytes32(0), "no commit");
        require(!info.revealed, "already revealed");

        bytes32 expected = computeCommitment(msg.sender, choiceHash, salt);
        require(expected == info.commitHash, "bad reveal");

        info.revealed = true;

        // Outcome-specific logic would go here.
        emit Revealed(msg.sender, choiceHash);
    }

    /// @notice Finalize after reveal deadline. Returns deposits to revealers and penalizes non-reveal.
    function finalize(address[] calldata participants) external {
        require(phase == Phase.Reveal || phase == Phase.Finalized, "wrong phase");
        require(block.timestamp > revealDeadline, "reveal still open");
        require(phase != Phase.Finalized, "already finalized");

        uint256 totalReveals = 0;
        uint256 totalNonReveals = 0;

        for (uint256 i = 0; i < participants.length; i++) {
            address user = participants[i];
            CommitInfo storage info = commits[user];
            if (info.commitHash == bytes32(0)) continue;

            uint256 dep = deposits[user];
            if (dep == 0) continue;

            if (info.revealed) {
                totalReveals++;
                deposits[user] = 0;
                // Return full deposit to revealer.
                (bool ok,) = user.call{value: dep}("");
                require(ok, "refund failed");
            } else {
                totalNonReveals++;
                deposits[user] = 0;
                // Penalize non-reveal. In production you might:
                // - send to treasury
                // - redistribute to revealers
                // - burn via a sink address
                // Here we keep funds in contract as protocol revenue.
            }

            // Mark consumed to prevent replay across rounds in a single-shot contract.
            info.commitHash = bytes32(0);
            info.revealed = false;
            info.commitTime = 0;
        }

        phase = Phase.Finalized;
        emit Finalized(totalReveals, totalNonReveals);
    }
}

Implementation notes that matter

This baseline implementation uses a choiceHash rather than raw choice data. That is deliberate. Choices can be complex: tuples, bids with metadata, move sequences, or lists. Normalizing into a hash keeps the commitment stable and avoids encoding mistakes. If you need to reveal the raw choice to the contract, you can still do it by hashing it on-chain and comparing to choiceHash, but do it carefully and avoid ambiguous packed encoding.

The baseline also uses a deposit to discourage non-reveal. Deposit size is a policy choice. If deposit is too small, griefing is cheap. If it is too large, participation drops. Many protocols implement variable deposits by role or by bid size so the penalty matches the harm.

Real world variants you will see in audits

In production you rarely see one generic commit-reveal contract. You see a commit-reveal component embedded in a larger system. These variants have different attack surfaces. Understanding them helps you choose the right one and avoid mixing guarantees.

Variant A: sealed bid auction

A typical sealed bid auction uses commit-reveal like this:

  • Commit phase: bidders submit commitment hashes of (bid amount, salt, bidder address, auction id).
  • Reveal phase: bidders reveal bid amount and salt. Contract verifies and stores revealed bids.
  • Finalization: highest revealed bid wins. Non-reveal bids are ignored, often penalized.

The main risks are non-reveal griefing and denial of service via massive commitments. The usual mitigations are deposits, max bidders per address, and careful gas use in finalization.

Variant B: multi-party randomness

For randomness, multiple participants commit to random seeds, then reveal seeds. The combined seeds form the random output, often via XOR or hashing the concatenation. This reduces single-party control, but it introduces a classic issue: the last participant to reveal can choose whether to reveal at all. If they can observe others' reveals and decide to withhold their own, they can bias the final output by selecting between "with their seed" and "without their seed".

Mitigations include:

  • Deposits with strong penalties for non-reveal.
  • Commitment to reveal by a certain time with slashing on failure.
  • Using threshold signatures or VRFs for stronger randomness when possible.

Variant C: commit-reveal voting

Commit-reveal voting aims to reduce early vote influence. It can also be used to reduce bribery signals, but it is not a full bribery resistance solution. Voters can still reveal their vote later, and bribery can be structured around proofs. Voting systems also care about quorum and thresholds, so non-reveal can be weaponized to break quorum.

A safe pattern is to treat non-reveal as abstain and not count it toward quorum. That prevents some griefing but can still affect outcomes if participation is low. Deposits can improve reveal completion, but that changes governance participation incentives.

Variant D: turn-based games

In games, commit-reveal prevents an opponent from reacting instantly. But games require timeouts. If a player refuses to reveal, the game must continue or end with a default outcome. That default outcome must be robust to griefing. For example, if non-reveal always causes the opponent to win, a player can grief by committing then refusing, wasting time and gas. If non-reveal always causes a draw, a player can avoid losing by refusing to reveal. The right default depends on the game and on deposits or stakes.

Step-by-step checks before you ship

The checklist below is designed to be used like an audit preflight. You can run it while writing code, before code review, and again before deployment. If you can answer every item clearly, your commit-reveal component is far more likely to behave like you think it does.

Check 1: does the commit bind the right fields

  • Is the committer identity bound, either by including user address in the hash and storing msg.sender at commit time?
  • Is the round id included so commits cannot be moved between rounds?
  • Is contract domain separation included, at least chain id and contract address?
  • Is the choice represented unambiguously, ideally via abi.encode or a normalized choiceHash?
  • Is a secret salt required, and is it large enough and truly random?

Check 2: can reveals be stolen or replayed

  • Can a third party submit the same reveal parameters and gain credit?
  • Does reveal validate msg.sender as the committer, not just the hash match?
  • After reveal, is the commitment consumed so it cannot be revealed again?
  • If multiple commits per address are allowed, are nonces used to prevent overwrites and replay?

Check 3: is phase gating strict and safe

  • Are commits rejected after commit deadline?
  • Are reveals rejected before reveal phase and after reveal deadline?
  • Is there a finalization mechanism that works even with missing reveals?
  • Are time windows long enough to tolerate realistic user behavior and network conditions?

Check 4: can non-reveal break the system

  • What happens if 30 percent of participants do not reveal?
  • What happens if the leading participant does not reveal?
  • What happens if one participant commits many times to bloat storage?
  • Is there a deposit, stake, or fee that makes griefing expensive?

Pre-deploy quick scan

  • Hiding: salt required, choices not brute forceable.
  • Binding: no ambiguous encoding, no collisions, correct state machine.
  • Ownership: reveal cannot be stolen, commit cannot be stolen.
  • Progress: finalize works without all reveals.
  • Observability: events emitted for commits and reveals, round ids included.

Bug patterns and the fixes that actually stop them

Bug pattern What it looks like on-chain Impact Root cause Fix
No salt Commitments are guessable from small choice set Choice leaks before reveal Low entropy inputs Require 32 byte salt from secure RNG
Not binding sender Anyone can reveal and claim outcome Reveal theft Reveal credits msg.sender Store committer and require msg.sender match
Missing domain separation Same commit works across clones or forks Replay across instances Hash lacks chain and contract fields Include chainid and address(this)
abi.encodePacked collision Two different reveals verify same commit Binding breaks Ambiguous packed encoding Use abi.encode or typed data
Broken phase gating Commits accepted during reveal or reveals early Adaptive strategy enabled Missing state machine Strict phase enum checks
Non-reveal stalls protocol Finalize requires all reveals Griefing and deadlocks Outcome logic assumes cooperation Finalize after deadline, ignore or penalize non-reveal
Commit overwrite User commits again, overwriting previous commit Replay or outcome manipulation Mapping keyed too broadly Key by (roundId, user, nonce) and lock per phase

Tools and workflow for builders and auditors

Commit-reveal components usually sit inside bigger systems. The best workflow is to treat them as separate units with their own invariant checks. You want to validate correctness at three layers: specification, implementation, and deployed behavior.

Workflow 1: write the invariants in plain language

Before code, write statements you can test:

  • A reveal is valid only if it matches a prior commit by the same user in the same round.
  • After reveal, the commit cannot be used again.
  • Reveals outside the reveal window are invalid.
  • If a user fails to reveal, the system still finalizes and the user is penalized or ignored.
  • No one can learn the choice from the commitment during commit phase.

If you cannot write these clearly, you do not have a stable spec yet. The prerequisite reading Solidity Security Basics helps you translate these invariants into common Solidity risk categories.

Workflow 2: test for the attacks, not just for happy paths

A robust test suite includes:

  • Attempt to reveal from a different address using copied parameters.
  • Attempt to commit late and reveal early.
  • Attempt to brute force commitments with small choice sets when salt is missing.
  • Attempt to finalize with missing reveals and ensure system progresses.
  • Attempt to reuse a commitment after reveal and ensure it fails.

If you are building in a team, make these tests part of the PR gate. Commit-reveal bugs are often not visible in normal integration tests.

Workflow 3: verify deployed contracts and adjacent risks

Many commit-reveal systems involve tokens, reward distribution, or auctioned assets. Even if your commit-reveal is correct, a separate issue can still ruin users: upgradeable admin keys, hidden minting, fee changes, and withdraw locks. If you are researching deployed systems, use a structured scan workflow like the Token Safety Checker alongside your manual review so you do not miss obvious structural risk.

Workflow 4: build depth, not just pattern memorization

Commit-reveal sits at the intersection of cryptography assumptions, protocol design, and Solidity engineering. If you want to build the intuition that makes your designs less fragile, the fastest path is:

Ship commit-reveal with fewer blind spots

The best commit-reveal designs feel boring in audits because they answer the same questions clearly: what is bound, what is hidden, who can reveal, what are the deadlines, and how does the system progress if people do nothing.

Operational security and key management notes

Commit-reveal relies on one fragile piece: the salt. If a user loses the salt, they cannot reveal. If users reveal the salt early, they leak intent. If users reuse salts across rounds, they leak linkability. This is not just a smart contract problem. It is a user key management problem.

If your protocol targets large users, you should assume some users will use hardware wallets and structured operational flows. Hardware wallets like Ledger can help reduce private key compromise risk, but you still need a safe salt generation and storage plan. A simple recommendation is to generate salts client side using a cryptographically secure RNG, store them locally, and show a single-click export for backup. For high value workflows, encourage users to treat salts like credentials.

Visibility, monitoring, and how commit-reveal fails in production

Many commit-reveal systems fail not because the math is wrong, but because operations are wrong. Reveal windows close while users sleep. Gas spikes make reveal uneconomical. RPC outages cause missed deadlines. A production design anticipates these realities.

Monitoring checks you should run

  • How many commits happen per round, and what percentage reveals successfully?
  • How many users reveal in the last 10 percent of the reveal window?
  • Does reveal completion drop during volatile gas periods?
  • Are there clusters of non-reveal that correlate with certain wallets or regions?

These metrics tell you whether your protocol is fair in theory but broken in practice. If 40 percent of users never reveal, your scheme is not delivering the intended guarantee. You may need longer windows, stronger penalties, better UX, or a different primitive.

A lightweight line graph for reveal completion

The graph below is a conceptual visualization you can use in internal monitoring dashboards. It illustrates a common pattern: commits happen early, reveals bunch near the end, and non-reveal spikes during gas stress. You can replace the placeholder values with real data later without changing the structure.

Reveal completion over the reveal window (concept) X-axis: time in reveal window. Y-axis: cumulative revealed percent. 0 20% 40% 60% 80% 100% Start 25% 50% 75% End Gas stress zone Reveals cluster late Non-reveal rises if UX is weak

Putting it all together

Commit-reveal is one of those patterns that looks easy until you put it in a hostile environment. The pattern can reduce front running and intent copying, but it introduces new strategy spaces: non-reveal, timing games, and griefing. The safest designs treat timeouts and penalties as first-class features, not as afterthoughts.

If you remember only one thing, remember this: the contract must define what happens when users do not cooperate. If your system only works when everyone reveals, your system will eventually stop working. If your commitments are not salted, domain separated, and identity bound, your system will be exploited by copycats and replays.

For structured learning, revisit Blockchain Technology Guides and Blockchain Advance Guides. For baseline Solidity threat modeling, use the prerequisite reading Solidity Security Basics. If you want ongoing security playbooks and pattern reviews, you can Subscribe. And when you are researching tokens that integrate auctions, claims, or governance, do a structural scan with the Token Safety Checker.

FAQs

What are commit-reveal schemes in simple terms?

Commit-reveal schemes are two step protocols. First you submit a commitment that hides your choice, usually a hash. Later you reveal the choice and a secret salt so the contract can verify your reveal matches your original commitment. The goal is to prevent others from reacting to or copying your choice before it is finalized.

Do commit-reveal schemes stop MEV completely?

No. They mainly reduce attacks that depend on learning your intent early, like copying a bid or reacting to a game move. They do not stop all MEV. Validators can still reorder transactions, and reveal phase actions can still be targeted. Commit-reveal is a fairness tool, not a full privacy or MEV elimination system.

Why do commit-reveal schemes require a salt?

Without a salt, commitments are often brute forceable because many choices have low entropy. A salt adds hidden randomness so observers cannot guess your choice by hashing a small set of possibilities. A 32 byte salt generated by a secure RNG is a common safe default.

What is the most common bug in commit-reveal implementations?

Not binding the commitment and reveal to the original committer identity. If reveal credit goes to msg.sender and anyone can submit the reveal parameters, a watcher can copy the reveal from the mempool and steal the outcome. The fix is to bind the commitment to the user and require the same user to reveal.

How do you handle users who never reveal?

You must design a rule that lets the protocol progress after the reveal deadline. Common options are to ignore unrevealed commits, apply a deposit penalty, or allow finalization that treats non-reveal as abstain or default. The correct choice depends on the application, but doing nothing creates a griefing vector.

Should I use abi.encode or abi.encodePacked for commitments?

Prefer abi.encode for structured encoding because it avoids ambiguous packed collisions. If you use abi.encodePacked, you must ensure fixed-size boundaries and explicit separators. Many binding failures come from packed encoding collisions where two different inputs can produce the same bytes.

What should be included in the commitment for safety?

A strong commitment typically includes chain id, contract address, user identity, round id, the choice (or a normalized choiceHash), and a secret salt. This reduces replay across contracts or chains, prevents commitment theft, and preserves hiding.

Where can I learn the fundamentals that make commit-reveal easier to design safely?

Start with Solidity Security Basics, then build deeper protocol intuition through Blockchain Technology Guides and Blockchain Advance Guides. For ongoing playbooks, you can Subscribe.

References

Official docs and reputable sources for deeper reading:


Final reminder: commit-reveal is about controlled disclosure. It works when your commitment is salted, domain separated, and identity bound, and when your protocol finalizes safely even if users do nothing. If you want ongoing smart contract security playbooks, you can Subscribe.

About the author: Wisdom Uche Ijika Verified icon 1
Founder @TokenToolHub | Web3 Research, Token Security & On-Chain Intelligence | Building Tools for Safer Crypto | Solidity & Smart Contract Enthusiast