基于 Commit-Reveal 机制的加密暗盘提交协议

2420 字
12 分钟
基于 Commit-Reveal 机制的加密暗盘提交协议

基于 Commit-Reveal 机制的加密暗盘提交协议#

前置知识:Unity 基础、C# 网络请求、SHA-256 哈希概念。


一、当拍卖行没有隐私#

假设你正在开发一款 MMO 的拍卖行系统。玩家对稀有装备出价,价高者得。最直观的实现:

// 客户端 —— "反面教材"
[System.Serializable]
public class BidRequest
{
public int playerId;
public int itemId;
public int price; // 出价
}
// 发送到服务器
IEnumerator SubmitBid(BidRequest req)
{
string json = JsonUtility.ToJson(req);
using var www = UnityWebRequest.Post("https://game-server.com/auction/bid", json);
yield return www.SendWebRequest();
}

这段代码能跑,但它有一个致命问题:价格是明文

任何能抓包的人——同一个 Wi-Fi 下的玩家、网络中间人、甚至服务器端的运维——都能看到 price: 150,然后精准地出 151。

这就是我们面临的问题:怎么让玩家提交一个服务器能验证、但中间任何人都看不懂的出价?

答案就是 Commit-Reveal 协议。


二、攻击面分析#

在学防御之前,我们先站到攻击者的角度。如果你是作弊者,你能怎么搞

攻击 1:嗅探#

玩家A ── BidRequest { price: 150 } ──→ 服务器
玩家B 抓包看到 150
玩家B ── BidRequest { price: 151 } ──→ 服务器 ← 赢了

不需要改代码,开个 Fiddler 或 Wireshark 就行。这是最廉价的攻击。

攻击 2:前端篡改#

// 攻击者修改客户端代码
IEnumerator SubmitBid(int itemId)
{
int myPrice = GetOtherPlayersMaxBid(itemId) + 1; // 总是比别人高1
// ... 发送 myPrice
}

攻击 3:时序博弈#

出价窗口 0s ────────────────────────────── 30s 截止
诚实玩家在 5s 出价
攻击者在 29.9s 出价(看了所有人的之后再决定)

这是 MW2 效应,即谁最后出手谁赢。

原则#

从攻击面可以归纳出反作弊的三条原则:

  1. 网络上传输的数据不能让任何人看懂
  2. 客户端发什么,服务器都要能独立验证
  3. 先提交和看到内容之间要有强制性间隔

三、承诺#

3.1 直觉#

想象一场拍卖:

  1. 你写一个数字在纸条上,放进信封,封好。
  2. 信封交给裁判,裁判把它钉在公告板上。
  3. 所有人都能看到信封(证明”我交了”),但没人能看到里面(“不知道出多少”)。
  4. 时间到了,裁判统一拆封,比较数字。

这就叫 Commit(承诺)。

3.2 密码学对应#

在计算机里,这里的”信封”就是哈希函数:

明文 = "150|c0a8b1f2e3d4..." // 出价 + 随机 Nonce
哈希 = SHA-256(明文) // "信封"

为什么不能直接 SHA-256("150")

因为出价空间极小——可能只有 100-999 这 900 个值。攻击者可以预计算所有出价的哈希值(彩虹表),看到 0xabc... 一查就知道是 150。

Nonce(随机数)的作用就是让哈希不可预计算

SHA-256("150|c0a8...") ≠ SHA-256("150|d4e5...") // 同一个出价,不同 Nonce = 不同哈希

没有任何人能从哈希反推出你的出价,除非他们知道你的 Nonce。

3.3 客户端实现:CommitBuilder#

using System.Security.Cryptography;
using System.Text;
/// <summary>
/// 客户端:生成 Nonce 并构建 Commit。
/// </summary>
public static class CommitBuilder
{
/// <summary>
/// 生成密码学安全的 32 字节随机数。
/// </summary>
public static byte[] GenerateNonce()
{
byte[] nonce = new byte[32];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(nonce);
return nonce;
}
/// <summary>
/// 对 (出价, 数量, Nonce, 会话ID) 计算 SHA-256 承诺哈希。
/// </summary>
public static byte[] Build(int bid, int quantity, byte[] nonce, string sessionId)
{
string plaintext = $"{bid}|{quantity}|{Convert.ToBase64String(nonce)}|{sessionId}";
byte[] input = Encoding.UTF8.GetBytes(plaintext);
using var sha = SHA256.Create();
return sha.ComputeHash(input);
}
/// <summary>
/// 返回十六进制字符串,方便日志和网络传输。
/// </summary>
public static string BuildHex(int bid, int quantity, byte[] nonce, string sessionId)
{
byte[] hash = Build(bid, quantity, nonce, sessionId);
return Convert.ToHexString(hash); // .NET 5+
}
}

使用示例:

byte[] nonce = CommitBuilder.GenerateNonce();
string hashHex = CommitBuilder.BuildHex(
bid: 150,
quantity: 1,
nonce: nonce,
sessionId: "round-42"
);
// 发送 hashHex 到服务器,本地保存 (bid, quantity, nonce)
// 注意:不要把 nonce 发给服务器

3.4 客户端:NonceStore#

Nonce 必须保存在本地,揭晓阶段才能用。简单的持久化方案:

using UnityEngine;
/// <summary>
/// 客户端本地 Nonce 存储。用 PlayerPrefs 做简单持久化,
/// 防止揭晓前客户端重启丢数据。
/// </summary>
public static class NonceStore
{
private const string KEY_SESSION = "cr_session";
private const string KEY_BID = "cr_bid";
private const string KEY_QTY = "cr_qty";
private const string KEY_NONCE = "cr_nonce";
public static void Save(string sessionId, int bid, int quantity, byte[] nonce)
{
PlayerPrefs.SetString(KEY_SESSION, sessionId);
PlayerPrefs.SetInt(KEY_BID, bid);
PlayerPrefs.SetInt(KEY_QTY, quantity);
PlayerPrefs.SetString(KEY_NONCE, System.Convert.ToBase64String(nonce));
PlayerPrefs.Save();
}
public static (int bid, int quantity, byte[] nonce) Load()
{
int bid = PlayerPrefs.GetInt(KEY_BID);
int qty = PlayerPrefs.GetInt(KEY_QTY);
byte[] nonce = System.Convert.FromBase64String(PlayerPrefs.GetString(KEY_NONCE));
return (bid, qty, nonce);
}
public static void Clear()
{
PlayerPrefs.DeleteKey(KEY_SESSION);
PlayerPrefs.DeleteKey(KEY_BID);
PlayerPrefs.DeleteKey(KEY_QTY);
PlayerPrefs.DeleteKey(KEY_NONCE);
PlayerPrefs.Save();
}
}

3.5 服务器端:CommitStore#

using System.Collections.Generic;
/// <summary>
/// 服务器端:管理 Commit 的收集和冻结。
/// </summary>
public class CommitStore
{
private Dictionary<int, string> _commits = new(); // playerId → hashHex
private bool _frozen;
/// <summary>
/// 接收玩家的 Commit 哈希。冻结后拒绝新提交。
/// </summary>
public bool Add(int playerId, string hashHex)
{
if (_frozen) return false;
if (string.IsNullOrEmpty(hashHex) || hashHex.Length != 64) return false;
_commits[playerId] = hashHex;
return true;
}
/// <summary>
/// 截止时间到,锁定所有 Commit。此后不允许增删改。
/// </summary>
public void Freeze() => _frozen = true;
/// <summary>
/// 获取某个玩家的 Commit 哈希,用于揭晓验证。
/// </summary>
public string GetHash(int playerId) =>
_commits.TryGetValue(playerId, out string h) ? h : null;
/// <summary>
/// 本轮所有 Commit 的快照(所有玩家可见,排序一致)。
/// </summary>
public IReadOnlyList<(int playerId, string hash)> Snapshot()
{
var list = new List<(int, string)>();
foreach (var kv in _commits)
list.Add((kv.Key, kv.Value));
return list;
}
}

注意Freeze() 之后的 Snapshot() 广播给所有客户端,这就是”公告板上的信封”,即你可以看到谁提交了,但不知道他出了多少。


四、揭晓#

4.1 协议#

Commit 截止后进入 Reveal 阶段。每个玩家发送原始数据:

客户端 → 服务器: { playerId, bid, quantity, nonce }

服务器:

重算 SHA-256(bid|quantity|nonce|sessionId)
比较 重算结果 == 之前存的哈希
匹配 → 记录有效出价
不匹配 → 作弊,标记 + 踢出
超时 → 该玩家本轮无效

4.2 服务器端:RevealValidator#

using System.Text;
/// <summary>
/// 服务器端:验证 Reveal 阶段的提交。
/// </summary>
public class RevealValidator
{
private CommitStore _commitStore;
private string _sessionId;
public RevealValidator(CommitStore commitStore, string sessionId)
{
_commitStore = commitStore;
_sessionId = sessionId;
}
public enum RevealResult
{
Valid, // 哈希匹配,出价有效
HashMismatch, // 哈希不匹配 → 可能篡改
NoCommit, // 该玩家没提交过 Commit
Timeout // 超时未揭晓
}
/// <summary>
/// 验证揭晓。重算 SHA-256 并与 Commit 阶段存的哈希比对。
/// </summary>
public RevealResult Verify(int playerId, int bid, int quantity, byte[] nonce)
{
string storedHash = _commitStore.GetHash(playerId);
if (storedHash == null)
return RevealResult.NoCommit;
string recomputed = CommitBuilder.BuildHex(bid, quantity, nonce, _sessionId);
if (recomputed != storedHash)
return RevealResult.HashMismatch;
return RevealResult.Valid;
}
}

4.3 客户端:SubmitReveal#

/// <summary>
/// 客户端:发送 Reveal 到服务器。
/// </summary>
IEnumerator SubmitReveal()
{
var (bid, qty, nonce) = NonceStore.Load();
// 构造揭晓请求
string json = JsonUtility.ToJson(new RevealRequest
{
playerId = myPlayerId,
bid = bid,
quantity = qty,
nonceBase64 = System.Convert.ToBase64String(nonce)
});
using var www = UnityWebRequest.Post("https://game-server.com/auction/reveal", json);
www.SetRequestHeader("Content-Type", "application/json");
yield return www.SendWebRequest();
if (www.result == UnityWebRequest.Result.Success)
{
NonceStore.Clear();
}
}

关键点:即使攻击者此时抓包看到 bid=150,已经晚了,Commit 阶段早已截止,他没法回去改自己的 Commit 哈希。


五、完整协议#

5.1 协议时序#

客户端 A 服务器 客户端 B
| | |
[COMMIT] [COMMIT] |
|--- hash_a -------→|←------ hash_b -------|
| | |
⋮ ⋮ ⋮
⋮ [COMMIT 截止] ⋮
⋮ Freeze() + ⋮
⋮ 广播所有哈希 ⋮
⋮ | ⋮
[REVEAL] [REVEAL] ⋮
|--- (bid,nonce) --→| ⋮
| | Verify() ⋮
| | ⋮
⋮ ⋮ ⋮
⋮ [SETTLE] ⋮
⋮ 比较出价 ⋮
⋮ A 赢 ⋮

5.2 服务器状态机:SessionManager#

using System;
using System.Collections.Generic;
/// <summary>
/// 服务器端:管理一轮暗盘拍卖的完整生命周期。
/// </summary>
public class SessionManager
{
public enum State { Idle, CommitPhase, CommitClosed, RevealPhase, Settled }
public State CurrentState { get; private set; } = State.Idle;
public string SessionId { get; }
private CommitStore _commits = new();
private RevealValidator _validator;
private Dictionary<int, (int bid, int qty)> _revealedBids = new();
private float _stateEnterTime;
private float _commitDuration;
private float _revealDuration;
public SessionManager(string sessionId, float commitDuration, float revealDuration)
{
SessionId = sessionId;
_commitDuration = commitDuration;
_revealDuration = revealDuration;
_validator = new RevealValidator(_commits, sessionId);
}
/// <summary>
/// 每帧或每个 Tick 调用。驱动状态机自动推进。
/// </summary>
public void Update(float currentTime)
{
switch (CurrentState)
{
case State.Idle:
// 等待外部触发开始
break;
case State.CommitPhase:
if (currentTime - _stateEnterTime >= _commitDuration)
CloseCommitPhase();
break;
case State.CommitClosed:
// 立即转入 Reveal(也可以等一小段广播时间)
CurrentState = State.RevealPhase;
_stateEnterTime = currentTime;
break;
case State.RevealPhase:
if (currentTime - _stateEnterTime >= _revealDuration)
Settle();
break;
}
}
public void StartRound()
{
CurrentState = State.CommitPhase;
_stateEnterTime = UnityEngine.Time.time;
}
private void CloseCommitPhase()
{
_commits.Freeze();
CurrentState = State.CommitClosed;
BroadcastCommitSnapshot(); // 给所有客户端看信封列表
}
private void BroadcastCommitSnapshot()
{
var snapshot = _commits.Snapshot();
// 发送 snapshot 到所有已注册玩家
}
public bool TryCommit(int playerId, string hashHex) =>
_commits.Add(playerId, hashHex);
public RevealValidator.RevealResult TryReveal(int playerId, int bid, int qty, byte[] nonce)
{
if (CurrentState != State.RevealPhase)
return RevealValidator.RevealResult.Timeout;
var result = _validator.Verify(playerId, bid, qty, nonce);
if (result == RevealValidator.RevealResult.Valid)
_revealedBids[playerId] = (bid, qty);
return result;
}
private void Settle()
{
CurrentState = State.Settled;
// 找出最高有效出价
int winnerId = -1;
int maxBid = -1;
foreach (var kv in _revealedBids)
{
if (kv.Value.bid > maxBid)
{
maxBid = kv.Value.bid;
winnerId = kv.Key;
}
}
// 下发结果
BroadcastResult(winnerId, maxBid);
}
private void BroadcastResult(int winnerId, int winningBid) { /* ... */ }
}

5.3 客户端状态机:RoundController#

using System.Collections;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 客户端:驱动拍卖 UI 的 MonoBehaviour 状态机。
/// </summary>
public class RoundController : MonoBehaviour
{
[Header("UI")]
[SerializeField] private InputField _bidInput;
[SerializeField] private Button _commitButton;
[SerializeField] private Button _revealButton;
[SerializeField] private Text _statusText;
[SerializeField] private Text _timerText;
private enum ClientState { Waiting, CommitPhase, WaitingForReveal, RevealPhase, Done }
private ClientState _state = ClientState.Waiting;
private byte[] _currentNonce;
private float _phaseTimer;
public void OnServerStartRound(float commitDuration)
{
_state = ClientState.CommitPhase;
_phaseTimer = commitDuration;
_commitButton.interactable = true;
_revealButton.interactable = false;
_statusText.text = "请出价并提交承诺";
}
public void OnCommitButton()
{
if (_state != ClientState.CommitPhase) return;
int bid = int.Parse(_bidInput.text);
_currentNonce = CommitBuilder.GenerateNonce();
string hashHex = CommitBuilder.BuildHex(bid, 1, _currentNonce, "round-42");
NonceStore.Save("round-42", bid, 1, _currentNonce);
// 发送 hashHex 到服务器
StartCoroutine(SendCommit(hashHex));
_commitButton.interactable = false;
_statusText.text = "已提交,等待截止...";
}
public void OnServerCommitClosed()
{
_state = ClientState.RevealPhase;
_revealButton.interactable = true;
_statusText.text = "请揭晓你的出价";
}
public void OnRevealButton()
{
if (_state != ClientState.RevealPhase) return;
StartCoroutine(SubmitReveal());
_revealButton.interactable = false;
_statusText.text = "已揭晓,等待结果...";
}
private IEnumerator SendCommit(string hashHex)
{
// POST /auction/commit { playerId, hash: hashHex, session: "round-42" }
yield break;
}
private IEnumerator SubmitReveal()
{
var (bid, qty, nonce) = NonceStore.Load();
// POST /auction/reveal { playerId, bid, qty, nonce: base64(nonce) }
yield break;
}
private void Update()
{
if (_state == ClientState.CommitPhase)
{
_phaseTimer -= Time.deltaTime;
_timerText.text = $"提交剩余: {Mathf.CeilToInt(_phaseTimer)}s";
}
}
}

延伸阅读

基于 Commit-Reveal 机制的加密暗盘提交协议
https://blog.rouming-fei.top/posts/commit-reveal/
作者
废江流
发布于
2026-07-22
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
废江流
提灯寻影,灯到影灭。
分类
标签
站点统计
文章
15
分类
4
标签
18
总字数
37,248
运行时长
0
最后活动
0 天前

文章目录