bindbox-game/internal/api/admin/verify_draw.go
邹方成 81e2fb5a75
Some checks failed
Build docker and publish / linux (1.24.5) (push) Failing after 41s
feat(activity): 实现抽奖随机承诺与验证功能
新增随机种子生成与验证逻辑,包括:
1. 添加随机承诺生成接口
2. 实现抽奖执行与验证流程
3. 新增批量用户创建与删除功能
4. 添加抽奖收据记录表
5. 完善配置管理与错误码

新增测试用例验证随机算法正确性
2025-11-15 20:39:13 +08:00

140 lines
5.2 KiB
Go

package admin
import (
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"net/http"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
)
type verifyDrawRequest struct {
AlgoVersion string `json:"algo_version" binding:"required"`
IssueID int64 `json:"issue_id" binding:"required"`
DrawID int64 `json:"draw_id" binding:"required"`
ClientSeed string `json:"client_seed" binding:"required"`
ServerSeedHash string `json:"server_seed_hash" binding:"required"`
ItemsRoot string `json:"items_root" binding:"required"`
WeightsTotal uint64 `json:"weights_total" binding:"required"`
SelectedIndex int `json:"selected_index" binding:"required,min=0"`
RandProof string `json:"rand_proof" binding:"required"`
}
type verifyDrawResponse struct {
Valid bool `json:"valid"`
Message string `json:"message"`
ServerSeedHashExpected string `json:"server_seed_hash_expected,omitempty"`
ItemsRootExpected string `json:"items_root_expected,omitempty"`
}
func (h *handler) VerifyDrawReceipt() core.HandlerFunc {
return func(ctx core.Context) {
var req verifyDrawRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
cm, err := h.activity.GetIssueRandomCommit(ctx.RequestContext(), req.IssueID)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.GetIssueRandomCommitError, err.Error()))
return
}
if cm == nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.GetIssueRandomCommitError, "未找到承诺"))
return
}
serverSeedHashBytes := cm.ServerSeedHash[:]
itemsRootBytes := cm.ItemsRoot[:]
if hex.EncodeToString(serverSeedHashBytes) != req.ServerSeedHash {
ctx.Payload(&verifyDrawResponse{
Valid: false,
Message: "server_seed_hash 不匹配",
ServerSeedHashExpected: hex.EncodeToString(serverSeedHashBytes),
})
return
}
if hex.EncodeToString(itemsRootBytes) != req.ItemsRoot {
ctx.Payload(&verifyDrawResponse{
Valid: false,
Message: "items_root 不匹配",
ItemsRootExpected: hex.EncodeToString(itemsRootBytes),
})
return
}
rewards, err := h.activity.ListIssueRewards(ctx.RequestContext(), req.IssueID)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListIssueRewardsError, err.Error()))
return
}
if req.SelectedIndex >= len(rewards) {
ctx.Payload(&verifyDrawResponse{
Valid: false,
Message: "selected_index 超出奖励范围",
})
return
}
sel := rewards[req.SelectedIndex]
if sel.Weight <= 0 || (sel.Quantity != -1 && sel.Quantity <= 0) {
ctx.Payload(&verifyDrawResponse{
Valid: false,
Message: "selected_index 对应奖励不可抽",
})
return
}
proofBytes, err := hex.DecodeString(req.RandProof)
if err != nil {
ctx.Payload(&verifyDrawResponse{
Valid: false,
Message: "rand_proof 格式错误",
})
return
}
clientSeedBytes, err := hex.DecodeString(req.ClientSeed)
if err != nil {
ctx.Payload(&verifyDrawResponse{
Valid: false,
Message: "client_seed 格式错误",
})
return
}
master := unmaskSeed(cm.ServerSeedMaster, cm.IssueID, cm.StateVersion)
subInput := make([]byte, 16)
binary.BigEndian.PutUint64(subInput[:8], uint64(req.IssueID))
binary.BigEndian.PutUint64(subInput[8:], uint64(req.DrawID))
mac := hmac.New(sha256.New, master)
mac.Write(subInput)
serverSubSeed := mac.Sum(nil)
enc := encodeMessage(req.AlgoVersion, req.IssueID, req.DrawID, 0, clientSeedBytes, 1, itemsRootBytes, req.WeightsTotal)
entropy := hmacSha256(serverSubSeed, enc)
var final []byte
{
W := req.WeightsTotal
var counter uint64
for {
R := binary.BigEndian.Uint64(entropy[:8])
M := (uint64(^uint64(0)) / W) * W
if R < M {
final = entropy
break
}
counter++
cenc := make([]byte, len(enc)+8)
copy(cenc, enc)
binary.BigEndian.PutUint64(cenc[len(enc):], counter)
entropy = hmacSha256(serverSubSeed, cenc)
}
}
if !hmac.Equal(final, proofBytes) {
ctx.Payload(&verifyDrawResponse{
Valid: false,
Message: "rand_proof 验证失败",
})
return
}
ctx.Payload(&verifyDrawResponse{Valid: true, Message: "验证通过"})
}
}