基于 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 效应,即谁最后出手谁赢。
原则
从攻击面可以归纳出反作弊的三条原则:
- 网络上传输的数据不能让任何人看懂
- 客户端发什么,服务器都要能独立验证
- 先提交和看到内容之间要有强制性间隔
三、承诺
3.1 直觉
想象一场拍卖:
- 你写一个数字在纸条上,放进信封,封好。
- 信封交给裁判,裁判把它钉在公告板上。
- 所有人都能看到信封(证明”我交了”),但没人能看到里面(“不知道出多少”)。
- 时间到了,裁判统一拆封,比较数字。
这就叫 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"; } }}延伸阅读