关于以太坊的私钥碰撞

2023-01-18

如果你有 1/100000000 的机会暴富,你愿意尝试一下吗?

以太坊私钥

以太坊的账户完全由私钥控制,根据私钥可以推算出账户的地址。私钥是一个 64 位长度的 16 进制字符串,比如:

0xd110227375ab838e8743192d278c105e30f253c966987c50b754412c9b986fe3

你可以在任何支持以太坊账户的钱包,用这个私钥导入对应的账户。这个私钥对应的账户地址是:

0x00000006A3D4DA3A559829B1730603CAeE97cC3D

你也可以根据这个账户地址,在以太坊浏览器上查询到账户相关的交易记录等信息。

私钥碰撞

既然私钥只是一个字符串,那么有没有可能,随机生成一个字符转,这个私钥对应的账户地址,正好有钱?因为有了私钥就能够控制账户上的所有资产。以太坊上有钱的账户那么多,有的余额还特别高,万一正好生成了一个私钥,那岂不是直接暴富。

当然随机生成的字符串,必须是 64 位长度 16 进制。为了避免以太坊的 SDK 存在某些防碰撞的规则,我们可以直接粗暴一点生成,比如这样:

let s = "0123456789abcdef";
let hex = "0x";
for (let i = 0; i < 64; i++) {
  hex += s[Math.floor(Math.random() * 16)];
}

这样做足够直观,在字符串层面随机拼接出 64 位长度的。LeetCode 的简单题就是天天搞这种。虽然实际上以太坊并没有防碰撞的机制。

碰撞概率

我们来计算一下,随机生成私钥,碰撞到有效账户的概率。假如存在一个地址,我们想要正要随机生成这个地址的私钥,可能性是多大?

16 进制就代表每一位字符,和目标私钥一致的可能性是 1/16,一共 64 位长度,概率就是 (1/16)64

p = (1/16)^64
  = 1 / 16^64
  = 1 / 115792089237316195423570985008687907853269984665640564039457584007913129639936

把零头抹掉,只计算数量级的话:

p = 1 / 115792089237316195423570985008687907853269984665640564039457584007913129639936
  = 1 / 100000000000000000000000000000000000000000000000000000000000000000000000000000
  = 1 / 10^77

这是一个非常低的概率,中国中奖率最低的大乐透彩票,中奖概率是千万分之一,也就是 1/10^8。以太坊的私钥碰撞,想正好是某一个账户的可能性,相当于连续中 10 次彩票。这是几乎不可能发生的事情。

不过彩票的中奖是有速度限制的,比如需要 1 天才开奖。加密货币领域的私钥可不是,如果提高随机生成私钥的速度,能不能增加碰撞成功的概率呢?

目前比特币全网的哈希率是 270M TH/s。比特币挖矿的过程本身就是不断地做哈希计算,直到找到拥有 n 和前导 0 哈希值的字符串,所以比特币的哈希率也可以用来描述私钥的生成速率。做一下单位换算

r = 270M TH/s
  = 270000000 TH/s
  = 270000000000 GH/s
  = 270000000000000 MH/s
  = 270000000000000000 kH/s
  = 270000000000000000000 /s
  = 100000000000000000000 /s
  = 10^20

这个结果只保留了数量级。假如你拥有全球比特币矿机的算力,去做以太坊的私钥碰撞,每秒钟的概率是:

ps = p * r
   = 1 / 10^57
   = 1 / 1000000000000000000000000000000000000000000000000000000000

这仍然是一个低到遥不可及的可能性。如果碰撞持续尝试 1 年呢?

py = ps * 60 * 60 * 24 * 365
   = ps * 31536000
   = ps * 10000000
   = ps * 10^7
   = 1 / 10^50
   = 1 / 100000000000000000000000000000000000000000000000000

持续运行 1 亿年呢?

pb = py * 100000000
   = py * 10^8
   = 1 / 10^42
   = 1 / 1000000000000000000000000000000000000000000

可以看到,时间对于概率的增加幅度,是微乎其微的。计算能力也许有可能成倍增长,比如苹果 M 系列芯片,比 Intel 芯片高几倍速度的提升。但是在 70 个 0 的数量级面前,这样的提升仍然微不足道。

即使拥有 1 亿个地球的比特币算力,运行 1 亿年,也只是能再减少几个 1 后面的 0,远远达不到碰撞成功的目的。

尝试一下

虽然可能性非常小,但我还是想尝试一下。

我写了一个脚本 eth-collision/eth-collision-random,随机生成私钥后,根据私钥的账户地址,自动去 etherscan.io 上查询账户余额,如果余额大于 0 会把私钥输出到日志文件里。由于 Etherscan 对 API 的调用次数有限制,脚本会以每秒 20 个地址的速度去查询,也就是一天能尝试 172 万个地址。

这个速度不够快,有更好的办法吗?凡是第三方服务,大多数会有 API 速率的限制,想不受限制进行查询,只能自己运行一个以太坊节点了。以太坊节点最低的硬件要求,是 4 核 16G 内存 1T 硬盘。这样配置的服务器,在 Vultr 上的价格至少要 300 美元一个月。有点贵。而且运行起节点后,还需要自己写一写程序,根据每个块的交易信息统计出有余额的地址。整个周期会很长。

我注意到 这个页面 提供了以太坊上余额最多的地址列表,1 页 100 个,总共 100 页。我写了一个爬虫 eth-collision/eth-address-top-list 到这个页面上抓取数据,这样就能得到以太坊余额最多的 10000 个账户地址的列表。

根据抓到的 10000 个地址,程序就可以比较快地运行了,而不需要受到 API 速率的限制,也不需要自己跑全节点。由于不看好 JavaScript 的运行速度,我用 Golang 写了一个程序 eth-collision/eth-collision-match-address,启动 100 个协程来加快生成私钥的速度。程序会把 10000 个地址读到一个 map 里,然后判断随机生成的账户是否在目标列表内。JavaScript 的一个 for 循环也足够把 CPU 跑满,但是直觉上总感觉 Golang 会更快一点。

对了,Golang 中的 map 记得加互斥锁。因为 map 不是并发安全的。不过加锁会降低速率。为了解决需要加锁的问题,因为只有 10000 个地址,占不了多少内存,所以可以实例化多个 map,有多少个协程就有多少个 map,每个协程在自己的 map 上做验证。这样就能充分保证私钥的生成和验证效率。

10000 个地址是不是太少了?以太坊一共有那么多个地址。一共有多少个呢?从 Etherscan 统计目前有 230M 左右,大概 2 亿多个。

后来我发现了一个仓库 eth-collision/Wallet-private-key-collision-brute-force-tool,这个仓库里提供了一个 OneDrive 文件的下载链接,是一个包含 1.8 亿条账户地址的文件,这些账户都在链上有过交易记录。文件的压缩包有 4.4G 大,解压后 16G 左右,是 pkl 格式的文件,然后我用 Python 解析成了 txt 文件。

在得倒这么多账户地址的列表后,现在的问题就变成了,该怎么利用这些数据。

首先是像原先一样,把所有数据读到 map 里。这么多的数据读到内存里,占用的内存可能至少 16G,也就是需要 24G 内存的服务器。Vultr 上最便宜的符合要求的服务器,接近 150 美元一个月。况且那么大数据量,Golang 的查询效率也是个疑问,还得加锁,限制比较多。

有没有好的方案?用数据库的话,MySQL 是支撑不住亿级别的数据查询的,记得之前参与的一个项目,在上亿数据里做最简单的查询要 10 秒以上,还是单次查询。现在的需求是每秒钟查询上千次。Redis 呢?Redis 的查询效率应该是有保证的,可 Redis 还是会把所有数据读到内存里。在 Redis 的介绍里,1M 个 key 需要 85M 的内存,换算一下:

1M   -> 85M
180M -> 180 * 85M
     = 15300M
     = 15G

Vultr 上,16G 的 Redis 价格很高,比服务器贵多了,需要 480 美元一个月。至于更沉重一点的数据库,像 Elasticsearch 或者更专业一点的 HBase 之类,从数据库搭建到导入数据,然后处理数据,一整套下来确实麻烦了一点。服务器开销也不见得更低。

再后来,看到有个东西叫布隆过滤器,正好符合这样的使用场景。我试了一下,容量 2 亿条数据的布隆过滤器,容错率 10 亿分之一,在加载完 1.8 亿条数据后,导出的二进制文件,只占用 1G 的磁盘空间。加载到内存中,也只需要占用 1G。

这样的方案其实挺好了。我在服务器上运行碰撞程序,每个月需要 30 美元的 2核 4G 服务器,每个小时可以尝试 60M 个地址。每个月需要 600 人民币的 8核 16G 服务器,每个小时可以尝试 160M 个地址。

当然,用再好的服务器都没有用,用整个地球上的算力都没用。

靓号地址

以太坊的账号地址也有靓号一说,比如地址 0x00000006A3D4DA3A559829B1730603CAeE97cC3D 包含有 6 个前导 0。关于靓号地址可以参考这篇文章

我写了这个程序 eth-collision/eth-collision-find-address 来寻找靓号地址,一般来说,找到 6 个前导 0 的地址是非常容易的事情,在 Macbook Air M1 上几分钟就可以。包含 7 个前导 0,可能需要半个小时。包含 8 个前导 0,就需要一两天。

包含有 10 个前导 0 的地址,已经算是非常非常稀有了。不过当然靓号地址没什么用,你一旦知道私钥,这个账号就是你的了,给别人别人也不要。

在线手动碰撞

这是一个在线手动进行私钥碰撞的网页,可以根据私钥计算出公钥,也可以生成随机的公私钥对,并且点击链接能够直接跳转到 Etherscan 检查地址对应的余额。

地址:http://eag.smallyu.net

源码:smallyunet/eth-address-generator

页面是纯前端实现的,可以离线使用,不会有任何安全问题。