邹方成 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

145 lines
4.3 KiB
Go

package activity
import (
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"time"
)
func (s *service) ExecuteDraw(ctx context.Context, issueID int64) (*Receipt, error) {
cm, err := s.GetIssueRandomCommit(ctx, issueID)
if err != nil {
return nil, err
}
if cm == nil {
return nil, nil
}
master := unmaskSeed(cm.ServerSeedMaster, cm.IssueID, cm.StateVersion)
items, err := s.readDB.ActivityRewardSettings.WithContext(ctx).
Where(s.readDB.ActivityRewardSettings.IssueID.Eq(issueID)).
Order(s.readDB.ActivityRewardSettings.Sort).
Find()
if err != nil {
return nil, err
}
var snapshot []ReceiptItem
var total int64
for _, it := range items {
snapshot = append(snapshot, ReceiptItem{ID: it.ID, Name: it.Name, Weight: it.Weight, QuantityBefore: it.Quantity})
if it.Weight > 0 && (it.Quantity == -1 || it.Quantity > 0) {
total += int64(it.Weight)
}
}
if total <= 0 {
return nil, nil
}
drawId := time.Now().UnixNano()
clientSeed := make([]byte, 32)
_, _ = rand.Read(clientSeed)
nonce := uint64(1)
subInput := make([]byte, 16)
binary.BigEndian.PutUint64(subInput[:8], uint64(issueID))
binary.BigEndian.PutUint64(subInput[8:16], uint64(drawId))
mac := hmac.New(sha256.New, master)
mac.Write(subInput)
serverSubSeed := mac.Sum(nil)
enc := encodeMessage(cm.AlgoVersion, issueID, drawId, 0, clientSeed, nonce, cm.ItemsRoot[:], uint64(total))
entropy := hmacSha256(serverSubSeed, enc)
pos, proof := rejectSample(entropy, serverSubSeed, enc, uint64(total))
var acc uint64
var selIndex int
var selID int64
for i, it := range items {
if it.Weight <= 0 || !(it.Quantity == -1 || it.Quantity > 0) {
continue
}
w := uint64(it.Weight)
if pos < acc+w {
selIndex = i
selID = it.ID
break
}
acc += w
}
rec := &Receipt{
AlgoVersion: cm.AlgoVersion,
RoundId: issueID,
DrawId: drawId,
ClientId: 0,
Timestamp: time.Now().UnixMilli(),
ServerSeedHash: cm.ServerSeedHash[:],
ServerSubSeed: serverSubSeed,
ClientSeed: clientSeed,
Nonce: nonce,
Items: snapshot,
ItemsRoot: cm.ItemsRoot[:],
WeightsTotal: uint64(total),
SelectedIndex: selIndex,
SelectedItemId: selID,
RandProof: proof,
Signature: nil,
}
return rec, nil
}
func encodeMessage(algo string, roundId int64, drawId int64, clientId int64, clientSeed []byte, nonce uint64, itemsRoot []byte, weightsTotal uint64) []byte {
var buf []byte
buf = appendUint32String(buf, algo)
buf = appendUint64(buf, uint64(roundId))
buf = appendUint64(buf, uint64(drawId))
buf = appendUint64(buf, uint64(clientId))
buf = appendUint32Bytes(buf, clientSeed)
buf = appendUint64(buf, nonce)
buf = append(buf, itemsRoot...)
buf = appendUint64(buf, weightsTotal)
return buf
}
func appendUint32String(b []byte, s string) []byte {
bs := []byte(s)
nb := make([]byte, 4)
binary.BigEndian.PutUint32(nb, uint32(len(bs)))
b = append(b, nb...)
b = append(b, bs...)
return b
}
func appendUint32Bytes(b []byte, bs []byte) []byte {
nb := make([]byte, 4)
binary.BigEndian.PutUint32(nb, uint32(len(bs)))
b = append(b, nb...)
b = append(b, bs...)
return b
}
func appendUint64(b []byte, v uint64) []byte {
nb := make([]byte, 8)
binary.BigEndian.PutUint64(nb, v)
b = append(b, nb...)
return b
}
func hmacSha256(key []byte, msg []byte) []byte {
m := hmac.New(sha256.New, key)
m.Write(msg)
return m.Sum(nil)
}
func rejectSample(entropy []byte, subSeed []byte, enc []byte, W uint64) (uint64, []byte) {
var counter uint64
for {
R := binary.BigEndian.Uint64(entropy[:8])
M := (uint64(^uint64(0)) / W) * W
if R < M {
return R % W, entropy
}
counter++
cenc := make([]byte, len(enc)+8)
copy(cenc, enc)
binary.BigEndian.PutUint64(cenc[len(enc):], counter)
entropy = hmacSha256(subSeed, cenc)
}
}