私钥碰撞源码解析:从ETH地址推导到区块链安全审计
简介面向区块链安全研究、私钥碰撞与ETH地址生成方向的技术爱好者资源包内含基于C和CUDA的私钥碰撞源码并结合Python脚本进行数据后处理适合希望研究私钥枚举、地址匹配及GPU加速逻辑的开发者参考学习。资源共119个文件压缩包约8.55MB主体为25个头文件和21个C源文件另有6个Python脚本、1个CUDA文件及若干编译链接产物obj/exe/lib/dll等覆盖核心算法、工程配置与可运行程序便于按需查阅。内容预览中出现GPUEngine、KeyHunt、IntMod等关键模块可较完整地还原私钥碰撞项目的工程骨架并包含地址文件排序、hash160输出等辅助脚本。无论是研究碰撞算法效率还是分析ETH地址生成与排序流程都能找到对应实现包内同时包含VS工程文件与Makefile便于在不同环境复现构建。目前已有3325人学习下载适合具有一定C基础、想对照源码理解区块链私钥碰撞思路的读者。1. 私钥碰撞源码区块链ETH一次注定失败的扫描却值得亲手跑一遍“私钥碰撞”这四个字在区块链开发者看来等同于“彩票中奖”标题里的源码写的都是同一个循环——随机生成私钥、推导出地址、去比对链上余额指望手里的CPU也能像传说那样撞上一个有钱的ETH地址。我在某公司的钱包模块做密钥安全审计时也翻过这类源码把线程、批量、随机源调了一轮得到的结论是对固定地址的命中概率低到数学上约等于零但这条推导链路值得每个人亲手写一遍它能解释为什么私钥必须安全存放、为什么压缩公钥不能直接当地址用、为什么钱包地址大小写还会影响比对结果。这篇文章面向两类人想搞懂ETH私钥与地址关系的新手以及需要做密钥派生审计却不想被网上各种押注式碰撞器带偏的开发者。2. 从私钥到ETH地址的推导链路写碰撞器之前先算对这个20字节碰撞器写错绝大多数不是坏在“碰撞”环节而是坏在“地址推导”环节。你要比对的地址来自钱包钱包里的地址是按一套固定规则从私钥算出来的如果你的源码里少了某一步或者用了压缩公钥那么同一个私钥在你手里算出的地址和钱包显示的根本不是同一个。先把这条链路彻底打通再谈批量扫描。2.1 私钥到地址的四步secp256k1、Keccak-256、后20字节与校验和ETH地址的推导可以拆成四步第一步用 secp256k1 椭圆曲线把 32 字节私钥变成公钥未压缩格式是 65 字节首字节固定为0x04第二步把首字节去掉对剩下的 64 字节做 Keccak-256 哈希第三步取这个 32 字节哈希值的最后 20 字节第四步按校验和规则把 40 个十六进制字符的大小写重新编排。校验和不是可选项它是后面碰撞器能不能命中目标的关键。先看最小实现import os from coincurve import PublicKey from Crypto.Hash import keccak def derive_address(priv: bytes) - str: # 第一步secp256k1 私钥 - 公钥未压缩格式 65 字节 pub PublicKey.from_valid_secret(priv).format(compressedFalse) # 第二步Keccak-256 哈希传入 64 字节公钥主体必须去掉开头的 0x04 h keccak.new(digest_bits256) h.update(pub[1:]) digest h.digest() # 第三步取哈希结果最后 20 字节 return digest[-20:].hex() priv bytes.fromhex(00 * 31 01) print(派生地址:, derive_address(priv))这段代码里最容易被忽略的是pub[1:]。format(compressedFalse)返回的是 65 字节未压缩公钥首字节0x04只是椭圆曲线坐标的标记不是公钥本身。如果你把整个 65 字节丢进 Keccak算出来的地址和任何正常钱包都不一样。另一个关键是 Keccak-256 与 SHA3-256 是两个算法Python 标准库hashlib.sha3_256是另一种填充规则算出来的摘要不同这里必须用Crypto.Hash.keccak这类实现。私钥本身要求是 32 字节且小于椭圆曲线的 n 值正常os.urandom(32)生成的整数几乎不会超界但严谨起见还是要做一次越界判断。校验和地址通常被直接展示在钱包里它的作用是让地址自带检错能力。下面这个函数可以把 40 位小写地址转成校验和格式def to_checksum_addr(addr: str) - str: addr addr.lower().removeprefix(0x) h keccak.new(digest_bits256) h.update(addr.encode()) hx h.hexdigest() return 0x .join( c.upper() if int(hx[i], 16) 8 else c for i, c in enumerate(addr) )规则是先对 40 位小写地址再做一次 Keccak-256得到 40 个十六进制字符作为掩码掩码某一位 ≥ 8就把原始地址对应位置的字母转成大写否则保留小写。碰撞器里如果你把钱包导出的大小写混合地址直接放进目标集合而推导端输出的是纯小写地址那么即使私钥碰巧正确字符串比较也永远不会相等。所以后面所有处理都先统一为小写、去掉0x前缀这个习惯能替你省掉一整天的排错时间。2.2 2^256 与 2^160 的差距为什么碰撞率低到约等于零私钥空间是 2^256但 ETH 地址只有 160 位也就是 2^160 个可能地址。单看 2^160 已经是个天文数字更不用说私钥到地址是多对一的映射。对某个固定目标地址随机生成一个私钥恰好导出的概率是 1/2^160。生日悖论能把这个数字降到大约 2^80但它要求的是“任意两个地址相同”对“指定目标地址”完全不起作用。对固定地址碰撞2^160 就是不可逾越的上限。我用普通配置的机器实测过四核八线程的笔记本每秒大约能推导三到六万个地址就按每秒十万次来算一天 86400 秒一年约 3.15 万亿次也就是 2^41 量级。想要达到 2^80 次尝试需要大约 2^39 年这个数量级已经没法用常识描述了。所以网上流传的那种“扫一天扫到余额地址”的说法要么是伪造截图要么是把别人已泄露的私钥拿出来冒充碰撞结果。真正的安全问题从来不是有人能撞出你的地址而是你的私钥生成熵不足、助记词泄露、或代码里用了可预测的随机数。把这条链路彻底理解之后再看那些碰撞器源码时你会一眼识别出哪些是随手写的玩具真正可用的扫描器必须先把地址推导做对然后才是批量生成和比对效率。3. 用Python写一个最小可运行的私钥碰撞器批量生成、目标去重与命中处理理解推导链路之后碰撞器本身并不复杂。它的核心是一个循环批量生成私钥、推导地址、和内存里的目标地址集合比对。真正的工程问题在于批量大小怎么设、线程开多少、目标地址文件怎么组织以及用什么随机源。这一章我用一个可以直接保存运行的脚本配合参数说明把它讲透。3.1 最小碰撞器源码三个模块各司其职下面这个脚本可以直接存成collider.py运行目标地址放在同目录的targets.txt里每行一个地址大小写随意import os import sys from concurrent.futures import ThreadPoolExecutor from coincurve import PublicKey from Crypto.Hash import keccak CURVE_N 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 def addr_from_priv(priv32: bytes) - str: # 输入必须是 32 字节且数值小于 secp256k1 的阶 n pub PublicKey.from_valid_secret(priv32).format(compressedFalse) h keccak.new(digest_bits256) h.update(pub[1:]) return h.digest()[-20:].hex() def scan(batch_size: int, targets: set) - tuple | None: # 每个批次独立生成随机私钥避免跨线程共用随机状态 for _ in range(batch_size): priv os.urandom(32) # 极端小概率越界直接跳过 if int.from_bytes(priv, big) CURVE_N: continue addr addr_from_priv(priv) if addr in targets: return priv.hex(), addr return None def main() - None: # 统一转小写去前缀绕开校验和大小写差异 targets { line.strip().lower().removeprefix(0x) for line in open(targets.txt, encodingutf-8) if line.strip() } print(floaded {len(targets)} target addresses) # 线程数建议设为本机物理核心数或逻辑核数减一 with ThreadPoolExecutor(max_workers4) as pool: futs [pool.submit(scan, 5000, targets) for _ in range(4)] for f in futs: r f.result() if r: print(matched:, r) with open(found.txt, w, encodingutf-8) as fp: fp.write(f{r[0]} {r[1]}\n) sys.exit(0) if __name__ __main__: main()这个脚本的比对逻辑依赖addr in targets而 targets 是一个只读 setPython 的 set 在多线程下并发读是安全的。每个线程自己调用os.urandom(32)互不干扰不会出现多个线程从同一个伪随机序列里取数导致大量重复私钥的问题。推导出的地址同样转成小写与 targets 里的格式完全一致这就消除了校验和大小写可能带来的假阴性。运行前先准备targets.txt最简单的验证方式是放一个自己已知私钥的地址比如私钥为00...01那个测试私钥对应的地址这样碰撞器理论上确实可能命中——不过你很快会体会到那概率有多低。正常使用时targets 文件可以放几千个从链上公开数据导出的地址但请记住它只是教学验证。3.2 三个必须调的参数max_workers、batch_size 与 targets 文件组织第一个参数是max_workers。PublicKey.from_valid_secret是 C 扩展实现计算时会释放 GIL所以多线程能真正利用多核。但线程数开太多会导致 CPU 上下文切换和内存带宽竞争性能反而下降。我通常会先看 CPU 逻辑核心数再减一作为初始值如果目标是跑长时间任务还可以考虑用os.sched_getaffinity(0)动态获取可用核心。线程数在四核机器上从 3 到 4 差别不大真正影响吞吐的是下单比而不是线程数量。第二个参数是batch_size。它控制每个线程一次性连续生成多少个私钥再返回。批次太小线程频繁提交任务调度开销占比过高批次太大如果中途命中scan函数不会立即中断可能把整批 5000 个都算完才返回白白浪费算力。我更倾向把 batch_size 设在 2000 到 10000 之间并在扫描循环里加上一个全局命中标志位命中后其他线程的循环也尽快退出。下面这段是对原脚本的小改动from threading import Event stop_event Event() def scan_with_stop(batch_size: int, targets: set) - tuple | None: for _ in range(batch_size): if stop_event.is_set(): return None priv os.urandom(32) if int.from_bytes(priv, big) CURVE_N: continue addr addr_from_priv(priv) if addr in targets: stop_event.set() return priv.hex(), addr return None第三个参数是 targets 文件本身。内存里一个 40 字符的字符串加 set 的哈希表开销大概 100 到 200 字节如果 targets 有百万行内存就要吃掉 100MB 以上。对个人电脑来说还能接受但如果目标是几千万地址就必须换成bytes存储或磁盘索引。把地址转成bytes能省一半内存查询时把推导结果也用bytes.fromhex转成同样的字节格式再比对。这个优化不影响命中逻辑只在内存边界时才有必要。跑起来之后用time python collider.py看一眼吞吐。不用纠结和别人的数字对比因为 CPU 型号、coincurve 版本、线程数都会影响结果你只需要记录自己机器上的基线速度后续改动参数时可以横向对比。每秒能推导几万地址是正常水平远低于这个数字要怀疑随机源或推导链路有冗余操作。3.3 命中目标地址之后该做什么碰撞器本身只负责发现“某个随机私钥的地址落在目标集合里”它不负责转账也不该把转账逻辑写进去。真命中时私钥和地址会被写入found.txt接下来验证私钥有效性、离线签名、广播交易这些动作都应该放在与碰撞器完全隔离的环境里操作。实际工程中更常见的做法是碰撞器输出私钥后先用本地离线钱包导入验证一次地址是否真的匹配确认无误后再考虑后续动作。有一个细节值得强调碰撞器长时间运行后操作系统熵池如果没有足够随机事件补充os.urandom在内核层面会阻塞等待表现为吞吐突然下降。这属于正常保护机制不要为了“提速”改回伪随机数生成器那会让整个碰撞器的输出变成可预测序列失去意义。4. 私钥碰撞检测中的常见问题与排查校验和、限流与随机源陷阱把碰撞器真正跑起来比写代码更耗时的往往是排错。这一章整理了我在密钥安全审计中遇到过的五类实际问题按“现象、原因、解决”三步说清省得你对着日志一头雾水。4.1 目标地址大小写不一致导致零命中现象脚本跑了一整夜日志显示正常、吞吐正常但零命中手动单独取一个私钥验证时推导出的地址和钱包显示的地址看起来“差不多”却永远不相等。原因钱包展示的地址大多带校验和大写碰撞器输出的是纯小写。字符串比较是逐字符进行的哪怕只有一个字母大小写不同也判为不匹配。这是碰撞器最隐蔽的坑因为推导链路本身没有错错在比对前没做归一化。解决在加载 targets 时统一执行line.strip().lower().removeprefix(0x)推导端输出地址后也直接以小写形式参与比对。我的习惯是把归一化写成一个函数并加一条断言确保 targets 集合里没有任何一个元素还带大写或0x前缀。4.2 用 random 模块生成私钥导致命中注定为零现象日志显示地址在反复循环一段时间内出现大量重复地址或者两个线程跑出来的结果完全一致等于做重复功。原因Python 的random模块是梅森旋转伪随机数生成器它由种子决定序列一旦种子相同整个序列可复现。多线程场景下如果共用一个实例还会出现线程安全问题即便每个线程各建一个实例如果种子都取自当前时间也可能生成大量重叠序列。解决统一用os.urandom(32)作为私钥来源。它由操作系统内核提供熵每个线程调用互不干扰序列不可预测。这是碰撞器在安全性上唯一正确的选择没有任何参数可以替代。4.3 公钥格式用错导致同一个私钥推导出不同地址现象单独测一个已知私钥钱包导入后显示的地址和你推导出来的地址总是对不上换一个钱包验证结果还是不一致。原因公钥有压缩和非压缩两种表示。未压缩公钥是 65 字节压缩公钥只有 33 字节ETH 地址规范使用的是未压缩公钥去掉0x04前缀后的 64 字节主体。如果你的代码用了compressedTrueKeccak 输入就是 33 字节而非 64 字节哈希结果完全变了。解决在使用硬币库或类似的 secp256k1 实现时明确传compressedFalse并检查返回对象长度是 65 字节。这个错误几乎无法靠肉眼从地址看出只能通过测试向量排查。4.4 公共节点或API限流导致扫描中断现象脚本前几分钟正常跑了一两个小时开始出现超时、连接重置随后整个进程卡死或退出。表面看是网络问题实际是目标地址余额查询接口的调用频控被触发。原因很多新手写碰撞器时不是用本地地址集合比对而是每生成一个地址就请求一次第三方接口查询余额。免费接口的调用配额非常有限对每秒数万次的推导速度来说几秒钟就能打满限额。解决把“查询余额”从这个热循环里彻底移走。正确的做法是先离线批量生成并推导地址把结果落盘保存再用冷路径去对照节点或区块数据或者干脆把 targets 文件当作唯一比对源完全不依赖网络。我一般会先确认 targets 文件里的地址是自己可控的样本集再决定是否需要接入本地同步节点。4.5 目标地址集合过大导致内存溢出现象targets 文件从几千行增加到几十万行后进程内存占用从 100MB 飙升到 1GB 以上小内存机器直接 OOM 被系统杀掉。原因字符串对象在 Python 里有对象头开销set 还要为哈希表预留额外空间。40 字符的十六进制地址存储开销远大于 20 字节原始值。解决将目标地址统一转成bytes再放入集合查询时同样用bytes.fromhex转换。如果目标集合达到千万行级别就要用固定宽度二进制文件配合 mmap 按位置读取或改用嵌入式数据库做存在性判断。判断标准很简单地址数量级在百万以下用set[bytes]以上就必须上索引。5. 把碰撞器升级成密钥空间验证工具测试向量、熵检查与更值得做的事碰撞器跑通之后它的真正价值不是“撞余额”而是变成一套密钥派生验证工具。我会把最后的篇幅留给验证方法和边界结论。5.1 用测试向量验证推导链路是否可信先做最基础的链路自检对同一个私钥推导两次结果必须一致再换一个私钥结果必须不同。这能排除随机性造成的假象。代码很简单priv bytes.fromhex(11 * 32) a1 addr_from_priv(priv) a2 addr_from_priv(priv) assert a1 a2 print(两次推导一致:, a1)这只能证明脚本内部自洽还不能证明推导结果符合 ETH 地址规范。更严谨的验证是把该私钥导入任意一款本地钱包对比钱包显示的地址与你推导出的地址是否一致。这个动作做完推导链路才算真正可信。建议把这一步固化成自动化测试放进版本管理后续改动推导代码时随时回归。5.2 同一套源码换两个方向熵泄漏检查与定制地址搜索第一个值得做的方向是随机源质量检查。设计一个实验连续生成一亿个私钥记录每个私钥对应的地址是否重复。如果发现大量重复或部分规律性重复说明随机源在退化一旦随机源是可预测的所有私钥的安全性都不成立。这个检查不需要跑完一亿次你只管让它持续运行定期统计重复率即可。现实中真正被“碰撞”出来的钱包绝大多数不是因为算力够强而是因为私钥生成端的熵源已经被污染。第二个方向是把目标从“固定地址集合”替换成“地址前缀规则”这就变成常见的定制地址搜索工具。比如你希望得到一个以0xabcd开头的地址只需把命中条件从addr in targets改成addr.startswith(abcd)碰撞器立刻变成一台地址定制器。它的应用场景是生成便于识别的收币地址成功率随前缀长度指数下降但三到四个十六进制字符的前缀通常家用算力几小时就能找到。我在参与某跨平台系统的密钥管理模块时正是用这套碰撞器源码的变体做熵泄漏审计不追求命中某个人而是通过统计大量随机密钥的地址分布确认团队自研的钱包派生流程没有使用弱随机源。这件事的实际价值比“中彩票”高出太多。最后留下一个习惯每次拿到别人的钱包相关源码先跑测试向量再谈优化先证明推导正确再谈性能和参数。希望帮到你。本文还有配套的精品资源点击获取