前言
前段时间在为某个活动做准备的时候讨论到了投票的问题,而投票系统最大的问题就是如何防止刷票了。
一般能想到的最简单的方案就是记录用户的 IP 地址,限制每个 IP 地址只能投一次票。然而这是远远不够的,毕竟有些攻击者手里是有大量肉鸡的,成千上万个 IP 并不是难事。
受到区块链技术中工作量证明的启发,我们是不是能通过通过提高计算量来增加刷票的难度呢?
工作量证明
在比特币系统中,矿工需要找到一串数字,使得整个区块的内容加上这串数字的哈希值开头刚好有若干个 0 。因为哈希函数是不可逆函数(Trap-door function),因此矿工只能一个一个数字猜,而找出的这个数字便意味着矿工付出了一定的计算量。
在投票系统中,如果我们也采用类似的设计,要求每个客户投票时都需要付出一定的计算量,就能大大增加刷票的难度。其实并不能
投票系统中的 “工作量证明”
与加密货币的目的不同,我们的目的不是要建立去中心化的信任,而是要给投票行为增加一定的代价。
整个系统的逻辑不难构思。对于前端而言,在投票前从服务器请求一个特定的 Challenge String ,之后找出一个 Magic String 让 Challenge String 加上 MagicString 的哈希值结果开头有若干个 0 ,在投票时将 Magic String 一并发给服务器即可。对于服务器而言, Challenge String 可以是客户端的 IP 地址的加盐哈希值,在投票请求处理时验证客户端的 Magic String 是否合法也十分容易——只需要计算一次哈希值即可。
开始实践
最近正好在熟悉 Go 语言,于是就用 Go 来写了((
这里只贴出与主题相关的代码,其他的逻辑和本篇文章没有太大关系
后端:
func tellChallenge(w http.ResponseWriter, r *http.Request) { if !checkIp(getIp(r)) { w.WriteHeader(429) _, _ = fmt.Fprintf(w, "ip already voted") return } w.WriteHeader(200) log.Printf("Challenge request from %s\n", getIp(r)) _, _ = fmt.Fprint(w, getChallenge(getIp(r))) } // challenge string 为客户端 ip 的加盐哈希 func getChallenge(ip net.IP) string { bytes := sha1.Sum([]byte(ip.String() + "you_know_what_this_is_the_salt_114514")) return hex.EncodeToString(bytes[:]) } // 验证哈希结果 func checkChallenge(ip net.IP, magic string) bool { challenge := getChallenge(ip) bytes := []byte(challenge + magic) hash := sha256.Sum256(bytes[:]) for i := 0; i < 3; i++ { if hash[i] != 0 { return false } } return (hash[3] & 0xE0) == 0 }
前端:
// 找到符合规则的 Magic String function findMagic(challengeString) { if (challengeString !== null) { for (let i=0n; true; i++) { let result = sha256.array(challengeString + i.toString()) if (result[0] === 0 && result[1] === 0 && result[2] === 0 && (result[3] & 0xE0) === 0) { return i.toString() } } } return null }
完整的代码在 Gitea 上 ,部署好的服务地址在这里。读者有兴趣可以尝试一下投票(大概需要十分钟到一小时不等)。
总结
对于哈希结果开头 0 的个数要求,笔者其实试了很多次,最后才找到一个让前端耗时比较符合预期的。值得一提的是笔者发现,对结果开头的 0 的个数每增加一个,耗时结果似乎并不是原来的两倍。这个结果笔者目前还没有办法解释。
事实上,这种策略也并不能有效的阻止刷票——在有特定硬件的情况下找 MagicString 的速度会比普通的电脑快很多,因此有心人只需要付出一定的代价就可以继续刷票。
此外,这种策略让投票用户的体验下降了很多。总得来说,在投票中运用工作量证明并不是一个有效的手段。要求实用性的话,还是用 reCAPTCHA 吧((
发表回复