150 lines
5.4 KiB
Markdown
150 lines
5.4 KiB
Markdown
# 抽奖与公平性算法技术白皮书
|
||
|
||
## 1. 概述
|
||
|
||
本系统采用 **「承诺机制 (Commitment Scheme)」** 结合 **HMAC-SHA256** 算法,确保抽奖过程的**不可预测性**、**可验证性**和**不可篡改性**。
|
||
|
||
核心原则:
|
||
1. **事前承诺**:活动开始前生成随机种子并公布其哈希值(Commitment)。
|
||
2. **事后验证**:活动结束后公布种子明文(Reveal),用户可复算验证。
|
||
3. **确定性算法**:输入(种子 + 上下文)确定,输出必然唯一。
|
||
|
||
---
|
||
|
||
## 2. 核心机制:承诺方案
|
||
|
||
### 2.1 种子生成
|
||
每个活动 (`Activity`) 在创建或发布时,系统服务器端会生成一个高质量的 32 字节随机种子 (`ServerSeed`)。
|
||
|
||
```go
|
||
// 伪代码示例
|
||
seed := make([]byte, 32)
|
||
rand.Read(seed) // 使用 crypto/rand 生成强随机数
|
||
```
|
||
|
||
### 2.2 承诺哈希 (Commitment Hash)
|
||
在数据库确立活动数据的一瞬间,系统计算种子的 SHA256 哈希值,作为**承诺**存储并对用户可见(虽然前端可能选择性展示)。
|
||
|
||
$$ SeedHash = \text{SHA256}(ServerSeed) $$
|
||
|
||
此哈希值一经生成不可更改,确保了服务器无法在后续过程中偷偷替换种子来操纵结果。
|
||
|
||
### 2.3 验证凭据 (Receipt)
|
||
每次抽奖完成后,系统会生成一份数字凭据 (`ActivityDrawReceipts`),其中包含:
|
||
- `issue_id`: 期号
|
||
- `seed_hash`: 对应的种子哈希
|
||
- `nonce` / `salt`: 随机盐值或防重随机数
|
||
- `snapshot`: 当时的奖池状态快照(权重/格位)
|
||
|
||
---
|
||
|
||
## 3. 算法实现
|
||
|
||
### 3.1 无限赏 (Weighted Random)
|
||
|
||
适用于奖品无限库存或按权重概率抽取的模式。
|
||
|
||
**算法流程**:
|
||
1. **输入**:
|
||
- $Seed$: 全局活动种子
|
||
- $IssueID$: 期号
|
||
- $UserID$: 用户ID
|
||
- $Salt$: 每次请求生成的 16 字节随机盐值
|
||
- $Rewards$: 奖品列表,包含权重 $w_i$
|
||
|
||
2. **随机数生成**:
|
||
使用 HMAC-SHA256 派生出一个确定性的随机数 $R$。
|
||
|
||
$$ \text{payload} = \text{fmt.Sprintf("draw:issue:\%d|user:\%d|salt:\%x", IssueID, UserID, Salt)} $$
|
||
$$ H = \text{HMAC-SHA256}(Seed, \text{payload}) $$
|
||
$$ R = \text{BigEndianUint64}(H[0:8]) \pmod {\sum w_i} $$
|
||
|
||
3. **结果选择**:
|
||
遍历奖品列表,累加权重查找 $R$ 落在哪个区间。
|
||
|
||
**代码逻辑**:
|
||
```go
|
||
mac := hmac.New(sha256.New, seedKey)
|
||
mac.Write([]byte(fmt.Sprintf("draw:issue:%d|user:%d|salt:%x", issueID, userID, salt)))
|
||
sum := mac.Sum(nil)
|
||
rnd := int64(binary.BigEndian.Uint64(sum[:8]) % uint64(totalWeight))
|
||
```
|
||
|
||
### 3.2 一番赏 (Ichiban / Shuffle)
|
||
|
||
适用于“箱内抽赏”模式,奖品总量固定,位置固定,采用先洗牌后抽取的逻辑。
|
||
|
||
**算法流程**:
|
||
1. **输入**:
|
||
- $Seed$: 全局活动种子
|
||
- $IssueID$: 期号(每一个箱子是一个 Issue)
|
||
- $TotalSlots$: 总格位数(例如 80 发)
|
||
- $Rewards$: 初始有序的奖品列表(填充后的平铺列表)
|
||
|
||
2. **确定性洗牌 (Deterministic Shuffle)**:
|
||
使用 Fisher-Yates 洗牌算法,配合 HMAC-SHA256 生成的随机序列对奖品位置进行打乱。
|
||
|
||
对于 $i$ 从 $TotalSlots-1$ 到 $1$:
|
||
$$ \text{payload}_i = \text{fmt.Sprintf("shuffle:\%d|issue:\%d", i, IssueID)} $$
|
||
$$ H_i = \text{HMAC-SHA256}(Seed, \text{payload}_i) $$
|
||
$$ j = \text{BigEndianUint64}(H_i[0:8]) \pmod {(i+1)} $$
|
||
交换索引 $i$ 和 $j$ 的元素。
|
||
|
||
3. **结果获取**:
|
||
用户选择的格位号 $k$ (1-based) 对应洗牌后数组的索引 $k-1$ 处的奖品。
|
||
|
||
$$ Reward = ShuffledRewards[SelectedSlot - 1] $$
|
||
|
||
**特性**:
|
||
- **预定性**:只要种子确定,箱子那一刻的奖品排列就已注定,不论谁来抽、何时抽,第 N 格永远是那个奖品。
|
||
- **公平性**:HMAC 的均匀分布保证了洗牌的随机性。
|
||
|
||
---
|
||
|
||
## 4. 验证指南
|
||
|
||
为了验证系统的公平性,用户或监管方可以使用官方提供的验证工具(`VerifyTool.exe`)进行独立计算。
|
||
|
||
### 4.1 获取验证参数
|
||
从 API 或页面获取以下信息(活动结束后公开):
|
||
1. `server_seed_hex`: 服务器种子(十六进制)
|
||
2. `issue_id`: 期号
|
||
3. `user_id` & `salt`: (仅无限赏需要)
|
||
4. `slot_index`: (仅一番赏需要)
|
||
5. `reward_config`: 奖品配置列表(验证前需要构建相同的初始列表)
|
||
|
||
### 4.2 运行验证工具
|
||
|
||
**无限赏验证命令示例**:
|
||
```bash
|
||
./VerifyTool verify-unlimited \
|
||
--seed "WaitToReveal32BytesHex..." \
|
||
--issue 1001 \
|
||
--user 12345 \
|
||
--salt "RandomSaltHex..." \
|
||
--weights "10,50,200,500"
|
||
```
|
||
|
||
**一番赏验证命令示例**:
|
||
```bash
|
||
./VerifyTool verify-ichiban \
|
||
--seed "WaitToReveal32BytesHex..." \
|
||
--issue 2002 \
|
||
--slot 5 \
|
||
--rewards "A:2,B:4,C:10,D:64"
|
||
```
|
||
*(注:rewards 格式为 `奖项:数量`,如 A赏2个, B赏4个...)*
|
||
|
||
### 4.3 验证原理
|
||
验证工具内置了与服务器完全相同的算法逻辑(Go 源码编译)。输入相同的种子和上下文,必将输出相同的中奖结果。
|
||
|
||
---
|
||
|
||
## 5. 安全性声明
|
||
|
||
1. **种子保密**:`ServerSeed` 在存储层加密保存,仅在活动结束或特定审计时刻解密公开。
|
||
2. **结果不可逆**:无法通过哈希值反推种子。
|
||
3. **防预测**:
|
||
- 无限赏:引入了 `Salt`(真随机生成),即使用户猜到了种子,也无法预测下一次抽奖结果(因为 Salt 每次不同)。
|
||
- 一番赏:种子一旦确定,序列即确定。我们在活动开始前才生成种子和 Commitment,确保无人(包括管理员)能提前知晓排列。
|