китаев.tech

Commit-reveal схема

Как скрыть действие в Solidity через commit-reveal: сначала записать hash, потом раскрыть значение и salt для проверки.

Commit-reveal скрывает выбор до reveal-фазы

Commit-reveal — это двухфазная схема. Сначала пользователь отправляет commitment: hash от секретных данных. Позже он раскрывает исходные данные, а контракт проверяет, что они дают тот же hash.

Так можно скрыть ставку, голос, ход в игре или другое действие от mempool до момента раскрытия.


Пишем скрытое голосование

Создайте файл contracts/CommitRevealVote.sol:

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract CommitRevealVote {
    struct Commit {
        bytes32 commitment;
        bool revealed;
    }

    uint256 public immutable commitDeadline;
    uint256 public immutable revealDeadline;

    uint256 public yesVotes;
    uint256 public noVotes;

    mapping(address => Commit) public commits;

    event Committed(address indexed voter, bytes32 commitment);
    event Revealed(address indexed voter, bool support);

    error InvalidPhase();
    error AlreadyCommitted(address voter);
    error NoCommitment(address voter);
    error AlreadyRevealed(address voter);
    error EmptyCommitment();
    error InvalidReveal();
    error InvalidDuration();

    constructor(uint256 commitDuration, uint256 revealDuration) {
        if (commitDuration == 0 || revealDuration == 0) {
            revert InvalidDuration();
        }

        commitDeadline = block.timestamp + commitDuration;
        revealDeadline = commitDeadline + revealDuration;
    }

    function commit(bytes32 commitment) external {
        if (block.timestamp >= commitDeadline) {
            revert InvalidPhase();
        }

        if (commitment == bytes32(0)) {
            revert EmptyCommitment();
        }

        if (commits[msg.sender].commitment != bytes32(0)) {
            revert AlreadyCommitted(msg.sender);
        }

        commits[msg.sender] = Commit({
            commitment: commitment,
            revealed: false
        });

        emit Committed(msg.sender, commitment);
    }

    function reveal(bool support, bytes32 salt) external {
        if (block.timestamp < commitDeadline || block.timestamp >= revealDeadline) {
            revert InvalidPhase();
        }

        Commit storage userCommit = commits[msg.sender];

        if (userCommit.commitment == bytes32(0)) {
            revert NoCommitment(msg.sender);
        }

        if (userCommit.revealed) {
            revert AlreadyRevealed(msg.sender);
        }

        bytes32 expectedCommitment = hashVote(msg.sender, support, salt);

        if (userCommit.commitment != expectedCommitment) {
            revert InvalidReveal();
        }

        userCommit.revealed = true;

        if (support) {
            yesVotes += 1;
        } else {
            noVotes += 1;
        }

        emit Revealed(msg.sender, support);
    }

    function hashVote(address voter, bool support, bytes32 salt) public view returns (bytes32) {
        return keccak256(
            abi.encode(
                address(this),
                voter,
                support,
                salt
            )
        );
    }

    function result() external view returns (bool passed) {
        return yesVotes > noVotes;
    }
}

Этот контракт проводит голосование в две фазы. Во время commit-фазы он видит только hash, а во время reveal-фазы проверяет голос и salt.


Как это работает

commit(bytes32 commitment) — сохраняет hash выбора пользователя. По hash нельзя понять, голосовал пользователь “за” или “против”.

reveal(bool support, bytes32 salt) — раскрывает исходные данные. Контракт заново считает hash и сравнивает его с сохранённым commitment.

hashVote(msg.sender, support, salt) — связывает commitment с конкретным адресом. Другой пользователь не сможет раскрыть чужой commitment.

address(this) внутри hash — привязывает commitment к конкретному контракту. Такой же голос и salt нельзя безопасно переиспользовать в другом голосовании.

userCommit.revealed = true — защита от повторного reveal. Без неё один пользователь мог бы посчитать голос несколько раз.

commitDeadline и revealDeadline — разделяют фазы. Во время commit нельзя раскрывать, а после commit-фазы нельзя добавлять новые commitments.


Как посчитать commitment

Frontend должен посчитать hash до вызова commit:

typescript
import { ethers } from "ethers";

const voteContract = "0x1111111111111111111111111111111111111111";
const voter = "0x2222222222222222222222222222222222222222";
const support = true;
const salt = ethers.randomBytes(32);

const commitment = ethers.keccak256(
  ethers.AbiCoder.defaultAbiCoder().encode(
    ["address", "address", "bool", "bytes32"],
    [voteContract, voter, support, salt],
  ),
);

console.log("Commitment:", commitment);
console.log("Salt:", ethers.hexlify(salt));

Сначала пользователь отправляет commit(commitment). После окончания commit-фазы он вызывает reveal(support, salt).


Почему нужен salt

Если hash строить только от support, вариантов всего два: true и false. Любой наблюдатель легко переберёт оба значения и узнает голос.

salt добавляет случайность:

text
hash(true)                 -> легко перебрать
hash(true, random bytes32) -> практически невозможно угадать

Salt должен быть случайным и храниться у пользователя до reveal-фазы. Если пользователь потеряет salt, он не сможет раскрыть commitment.


Где применять commit-reveal

Commit-reveal подходит для скрытых ставок, sealed-bid аукционов, голосований, игр с ходами и защиты от фронтраннинга.

Схема не делает данные приватными навсегда. Она только откладывает раскрытие до reveal-фазы. После reveal все исходные параметры становятся публичными.

Для денежных сценариев добавляют deposit или штраф: если пользователь сделал commit, но не раскрыл reveal, его депозит можно частично или полностью забрать.


Частые ошибки

Не добавлять salt → маленькое пространство вариантов легко перебрать, и commitment перестаёт скрывать выбор.

Не включать msg.sender в hash → другой адрес сможет раскрыть чужой commitment с теми же данными.

Разрешить commit и reveal одновременно → наблюдатель сможет увидеть reveal и попытаться вставить свой commit до него. Фазы должны быть разделены.


Что дальше

Материалы китаev.tech публикуются в образовательных целях и не являются инвестиционной рекомендацией. Примеры кода и описания протоколов — для обучения; использование в продакшне на ваш собственный риск. Дисклеймер и политика конфиденциальности.