Java+Web3j实现助记词生成地址与以太坊余额查询
简介面向以太坊开发者和区块链安全研究者的Java工具工程基于web3j实现直连以太坊节点自建节点或公共RPC均可使用核心功能是根据助记词生成地址再直接与节点交互查询余额并记录结果。其中助记词遍历部分按生成规则进行了反推判断能把4.8亿种单词组合情况压减至0.3亿种效率提升约16倍相当于直接运行一套本地硬破解流程。资源包为zip压缩格式共53个文件主体由29个jar依赖库构成另含7个Java源码、7个class编译产物、2个txt说明、配置及properties文件等整体约19.79MB可直接导入开发环境阅读与调试。目前已有4183人学习/下载具备不错的参考热度。借助源码可掌握web3j与以太坊节点交互、BIP39助记词派生地址、余额批量查询以及遍历剪枝优化思路工程内还集成了常用加密、JSON和数据库相关依赖适合在此基础上二次开发用于密钥找回、合规审计或教学实验。1. 助记词碰撞器别急着写先想清楚你要的是哪一段链路java web3j 助记词生成地址 直连以太坊节点 查余额 硬破解这组关键词拼在一起很多人第一反应是“助记词碰撞器”——一个不停生成助记词、推导地址、然后去链上查余额的脚本。但真把它拆开看最值钱的不是最后那步“碰撞”而是从助记词到地址、再从地址到余额的整条链路能不能在自己手里闭环。网上把碰撞器说得再玄学底层也不过是 BIP39 助记词生成、BIP44 路径推导、JSON-RPC 余额查询这三段基础功。这篇文章写给两类人一是刚接触 web3j想用 Java 把钱包地址生成和链上查询跑通的新手二是已经能写简单调用、但想把“扫描器”做对、做得可验证的从业者。我不会把碰撞说得天花乱坠因为 128 位熵在算力上根本不支持无差别硬破解真正可行的是恢复自己丢失的助记词、校验钱包派生路径、做受控的安全测试。下面按一条能复现的路径走先搞懂推导链再直连节点查余额最后才讨论扫描器的边界和参数。不管你对标题里哪一段感兴趣前半段的链路都是绕不开的地基。2. 助记词到地址BIP39/BIP44 推导链路与 web3j 最小实现2.1 助记词不是私钥先搞懂 熵→助记词→种子→私钥→地址 这条推导链很多人以为助记词就是私钥的另一种写法其实中间隔了好几层。要理解标题里的“助记词生成地址”得先把 BIP39 和 BIP44 这两套标准的关系理清楚——这也是 java 面试题里常被问到的知识点八股文背一百遍不如自己跑一遍。BIP39 做的事情是“熵到助记词”系统先随机生成一段熵比如 128 位在这段熵后面追加 4 位校验和然后按每 11 位一组去英文词表里查词得到 12 个单词。所以 12 个助记词不是 12 个随机词它内部带了校验信息任何一个词写错都能被程序识别出来。web3j 里对应的类是MnemonicUtils我们后面会直接用到它。拿到助记词之后下一步是把它变成种子。BIP39 规定用 PBKDF2 算法对助记词文本做拉伸可以带一个额外的口令passphrase没口令就传空字符串。种子再交给 BIP32生成主私钥和主链码。到这里为止助记词和私钥之间才第一次建立关系——它不是简单的哈希而是一套确定性的推导过程。最后一步是 BIP44它规定了以太坊的钱包路径m/44/60/0/0/0。这段路径里的44表示 BIP44 标准60是以太坊的 coin type后面三段分别是账户索引、找零标记和地址索引。路径不同派生出来的私钥就完全不同。同一组助记词用m/44/60/0/0/0和用m/44/60/0/0/1得到的是两个不同地址这也是后面最容易翻车的地方。2.2 用 web3j 生成助记词与地址最短可跑代码不引入额外依赖只用 web3j 的 crypto 包就能把“助记词→种子→私钥→地址”的链路完整跑一遍。下面这段是我平时用来验证环境的最小代码核心是MnemonicUtils和Bip32ECKeyPair。import org.web3j.crypto.Bip32ECKeyPair; import org.web3j.crypto.Credentials; import org.web3j.crypto.MnemonicUtils; import java.security.SecureRandom; // 16 字节 128 位熵web3j 会自动追加 4 位校验位得到 12 个助记词 byte[] entropy new byte[16]; new SecureRandom().nextBytes(entropy); String mnemonic MnemonicUtils.generateMnemonic(entropy); // 空口令生成种子PBKDF2 默认迭代次数是 2048 byte[] seed MnemonicUtils.generateSeed(mnemonic, ); // BIP44 路径 m/44/60/0/0/0注意 hardened 位要用 0x80000000 标记 int[] path { 44 | 0x80000000, 60 | 0x80000000, 0 | 0x80000000, 0, 0 }; Bip32ECKeyPair masterKeypair Bip32ECKeyPair.generateKeyPair(seed); Bip32ECKeyPair derivedKeypair Bip32ECKeyPair.deriveKeyPair(masterKeypair, path); Credentials credentials Credentials.create(derivedKeypair); System.out.println(助记词: mnemonic); System.out.println(地址: credentials.getAddress()); System.out.println(私钥: credentials.getEcKeyPair().getPrivateKey().toString(16));这段代码里最关键的是int[] path的写法。44 | 0x80000000表示 44 这个数字加上 hardened 标记写成十进制是2147483692写成十六进制是0x8000002C。如果你只写44, 60, 0, 0, 0不加 hardened 位推导出来的地址和 MetaMask、imToken 这些主流钱包对不上——因为标准路径里前三级都是 hardened 的。地址的生成逻辑在Credentials.create内部完成私钥先算出公钥公钥做 Keccak-256 哈希取后 20 字节转成十六进制再补上0x前缀。这段逻辑 web3j 封装好了但你要知道它内部是这样推导的后面排查“地址对不上”的时候会用到。2.3 自定义 BIP44 路径取地址不落盘、实时推导的写法Bip39Wallet.create这个方法也能生成钱包但它会把 keystore 文件写到磁盘上扫描场景里这么玩会被文件 IO 拖死而且它用的是默认路径不灵活。我一般不用它而是像上面那样手动控制路径好处有两个一是地址不用落盘内存里实时算二是路径可以随意换恢复多账户钱包时能逐个账户扫。// 遍历 BIP44 路径的第 5 层恢复一个钱包的多个收款地址 for (int index 0; index 10; index) { int[] accountPath { 44 | 0x80000000, 60 | 0x80000000, 0 | 0x80000000, 0, index }; Bip32ECKeyPair derived Bip32ECKeyPair.deriveKeyPair(masterKeypair, accountPath); String address Credentials.create(derived).getAddress(); System.out.println(索引 index - address); }这段代码的masterKeypair从上一步复用不用为每个地址重新生成一次种子。注意Bip32ECKeyPair.deriveKeyPair是纯内存计算一秒钟跑几千次没问题批量恢复钱包时完全不用考虑性能。我建议你拿到任何一组助记词先用这段代码把前 5 个地址打印出来再和你的手机钱包核对一遍。路径一致、地址一致才说明你的推导链路是干净的。这一步花不了两分钟但能避免后面所有匪夷所思的“余额不对”问题。3. 直连以太坊节点查余额从 RPC 选型到批量查询3.1 直连节点 vs 第三方 API为什么把 RPC 地址握在自己手里查余额这件事最省事的做法是调第三方 API注册一个 key然后拿着地址去请求。但批量场景里第三方 API 有三个问题绕不开并发限制、隐私暴露、还有不可控的限流策略。你跑扫描器跑到一半被限流整个节奏全乱了。直连自建节点没有这些问题——节点是你自己的并发和频率完全由你控制。直连的代价是需要一台能跑节点的机器。本地开发用 geth 起一个测试节点就够了不需要同步主网全部数据--dev模式会给你一个即时出块的测试环境。等逻辑验证完再决定要不要连主网节点。geth --dev --http --http.addr 127.0.0.1 --http.port 8545 --http.api eth,net,web3--http.api这里只开了eth,net,web3注意没有personal。旧版本 geth 的personalAPI 能直接管账户但新版本默认不开放而且用 web3j 的Credentials就能完成签名没必要走personal。--http.addr绑定127.0.0.1是安全习惯机器上有别的服务时不要随便绑0.0.0.0。3.2 web3j 直连节点查余额最小可跑代码与连接池参数连接本地节点用HttpService就够了但默认的HttpService直接 new 会自己创建一个 OkHttpClient连接池参数不可控批量查询时容易把节点连接打满。我一般会自己构造 OkHttpClient把超时和连接池显式设好。import okhttp3.ConnectionPool; import okhttp3.OkHttpClient; import org.web3j.protocol.Web3j; import org.web3j.protocol.http.HttpService; import org.web3j.protocol.core.DefaultBlockParameterName; import org.web3j.protocol.core.methods.response.EthGetBalance; import java.math.BigInteger; import java.util.concurrent.TimeUnit; OkHttpClient client new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .connectionPool(new ConnectionPool(20, 5, TimeUnit.MINUTES)) .build(); Web3j web3j Web3j.build(new HttpService(http://127.0.0.1:8545, client, false)); EthGetBalance response web3j .ethGetBalance(0x..., DefaultBlockParameterName.LATEST) .send(); if (response.getError() ! null) { System.out.println(RPC 错误: response.getError().getMessage()); return; } BigInteger balanceWei response.getBalance(); System.out.println(余额(wei): balanceWei);readTimeout设 30 秒是因为节点在同步或忙时响应会变慢设太短会误报超时。connectionPool的 20 表示最多保持 20 条空闲连接5 分钟是空闲回收时间批量扫描时这个值够用。HttpService第三个参数false表示不用 WebSocketHTTP 轮询足够。DefaultBlockParameterName.LATEST是查最新已确认区块的余额还有PENDING可以查包含待确认交易的余额。如果只做资产统计用LATEST就行PENDING的数字可能包含还没落地的交易。3.3 批量查余额的节奏并发数、超时与重试之间怎么取舍单次查询好写真正麻烦的是批量。web3j 的同步接口是阻塞式的你写个 for 循环一个个查几千个地址要跑很久直接上多线程又可能把节点打挂。这里的取舍我踩过不少坑最后的经验是单节点并发控制在 10 到 20 之间用信号量限流比用固定延迟更高效——延迟会让整体吞吐上不去而信号量只是限制峰值的并发数节点没满的时候依然能跑满。import java.util.concurrent.Semaphore; // 信号量限流最大 15 个并发请求 Semaphore rpcLimiter new Semaphore(15); for (String address : addressList) { rpcLimiter.acquire(); executor.submit(() - { try { EthGetBalance response web3j .ethGetBalance(address, DefaultBlockParameterName.LATEST) .send(); // 处理结果失败时记录并稍后重试 } catch (Exception e) { // 记录失败地址退避后重试 } finally { rpcLimiter.release(); } }); }重试策略上不要一失败立刻重试节点可能正在忙。常见的做法是退避重试第一次失败等 1 秒第二次等 2 秒第三次等 4 秒超过三次就单独记到失败列表里等这批跑完再单独处理。千万别在循环里 sleep 固定 100 毫秒来“限速”那既浪费吞吐又没法应对节点偶发抖动。4. 硬破解扫描器的真实边界碰撞逻辑、算力假设与受控实现4.1 助记词空间有多大为什么硬破解不是想的那样标题里的“硬破解”三个字让不少人误以为这个工具能拿来找别人的钱包。这件事的数学边界必须说清楚12 个助记词对应 128 位熵地址空间是 2 的 128 次方。就算你的机器每秒能生成并推导 1000 个地址一年也才跑 3 乘 10 的 10 次方个距离 2 的 128 次方差了十几个数量级。结论很明确无差别的助记词碰撞在物理上不可行任何声称能“扫全网钱包”的工具要么是骗局要么只在你本地跑了个演示动画。所以“硬破解”的正确理解是恢复自己丢失的助记词。比如你记得助记词的一部分或者确定了路径范围用程序去补齐剩余部分又比如你想验证某个助记词是不是对应某笔历史转账的地址。这类受控场景才有实际意义。下面这段代码也只面向这个目的目标列表存的是你自己的地址bound 参数用来限制扫描次数跑完即停。4.2 扫描器主循环助记词生成→地址推导→目标比对 三步扫描器的主循环不复杂就是 2.2 节那段推导逻辑加一个地址比对。但有几个细节我是在翻车之后才补上的一是命中后必须立即写文件不能攒在内存里等最后统一写否则进程一崩全丢二是目标地址统一转小写web3j 返回的地址是带校验和的大写混合格式不统一的话每次都匹配不上。import java.io.FileWriter; import java.security.SecureRandom; import java.util.Set; // bound 是有界扫描次数目标列表只放你自己拥有的地址 int bound 10_000; SetString targets Set.of(0x你的地址1.toLowerCase(), 0x你的地址2.toLowerCase()); SecureRandom secureRandom new SecureRandom(); int found 0; for (int i 0; i bound found targets.size(); i) { byte[] entropy new byte[16]; secureRandom.nextBytes(entropy); String mnemonic MnemonicUtils.generateMnemonic(entropy); byte[] seed MnemonicUtils.generateSeed(mnemonic, ); int[] path {44 | 0x80000000, 60 | 0x80000000, 0 | 0x80000000, 0, 0}; Bip32ECKeyPair derived Bip32ECKeyPair.deriveKeyPair( Bip32ECKeyPair.generateKeyPair(seed), path); Credentials credentials Credentials.create(derived); String address credentials.getAddress().toLowerCase(); if (targets.contains(address)) { found; // 命中后立即追加写文件避免进程崩溃丢结果 try (FileWriter writer new FileWriter(hit.csv, true)) { writer.write(mnemonic , credentials.getEcKeyPair().getPrivateKey().toString(16) , credentials.getAddress() \n); } } } System.out.println(扫描完成命中 found 个);这个循环的耗时主要在MnemonicUtils.generateSeed上PBKDF2 是故意设计得很慢的目的是防暴力破解。一万次循环在普通笔记本上大概要跑几十秒到几分钟这个速度对“恢复自己钱包”完全够用对“碰运气扫别人地址”则没有任何意义。代码里bound就是硬性上限我建议开始调试时先设成 1确认输出没问题再往上加。4.3 多线程与资源分配CPU 密集与 IO 密集要分开看扫描器做到后面必然要上并发但有个原则容易被忽略助记词生成和地址推导是 CPU 密集操作查余额是 IO 密集操作它们不应该混在同一个线程池里。CPU 密集的线程池开太多上下文切换会吃掉性能IO 密集的线程池开太少节点带宽又利用不起来。import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; // 生成线程池CPU 核数减 1只做助记词生成和地址推导 ExecutorService derivePool Executors.newFixedThreadPool( Runtime.getRuntime().availableProcessors() - 1); // 查询线程池独立于生成逻辑控制 RPC 并发 ExecutorService queryPool Executors.newFixedThreadPool(15);我一般把生成和比对放在推导线程池里命中之后把地址提交给查询线程池去查余额两个池子之间用阻塞队列解耦。这样做还有一个好处查询节点慢的时候生成逻辑不会被阻塞吞吐瓶颈始终只在某一个环节上好定位。5. 避坑与排查助记词工具链最常见的 5 个翻车点5.1 助记词和地址对不上BIP44 路径少了一层现象同一个助记词web3j 生成的地址和手机钱包显示的不一样而且完全没有规律。原因路径写错了。最常见的是把m/44/60/0/0/0写成了m/44/60/0/0少了最后一级地址索引或者前三级忘了加0x80000000hardened 标记。BIP44 的路径差一层派生出的私钥就完全是另一个这种错代码不会报异常只能靠对比结果发现。解决打印出完整的int[]对照m/44/60/0/0/0逐级检查。调试时固定一份已知助记词把 web3j 生成的地址和手机钱包对比一致再继续。5.2 助记词校验和过不了单词顺序和大小写都是坑现象手动输入助记词后程序抛异常或者生成的地址不对。原因BIP39 的助记词是区分大小写的且词表固定为 2048 个英文单词。单词拼错、顺序颠倒、大小写不一致都会导致校验和失败。另外MnemonicUtils在校验失败时抛的是运行时异常很多时候你看到的不是“词写错了”而是别的诡异报错。解决在生成地址前先调MnemonicUtils.validateMnemonic(mnemonic)做一次显式校验如果返回 false 直接打印“助记词非法”不要把错误带到下一步。这样至少能区分是输入问题还是推导问题。5.3 直连节点查余额一直为 0先确认节点同步状态现象地址明明有币直连自己的 geth 节点查出来余额却一直是 0。原因节点没有同步完成。主网全量节点同步到最新高度需要很长时间在同步到目标地址所在的区块之前节点返回的余额就是不完整的 0。这个问题不会报错误码排查起来特别容易忽略。解决查余额前先调web3j.ethSyncing().send()检查返回值。返回false说明已同步完成返回对象说明还在同步中看currentBlock和highestBlock的差距。开发调试阶段直接用geth --dev起一个即时出块的测试节点省去同步等待。5.4 批量查询触发节点限流连接被重置现象单条查询正常批量跑几十个请求后节点开始超时或直接连接重置。原因每发一次请求都新建 HTTP 连接没有复用连接池或者并发数超过了节点处理能力的上限geth 的 RPC 服务端有最大并发请求限制达到上限后会拒绝新连接。解决用 OkHttp 连接池复用连接把并发降到 10 到 15超时时间保持 30 秒以上。看 geth 日志里有没有http: panic serving之类的字样如果频繁出现把ConnectionPool的空闲连接数往下调同时给信号量的 permit 数减半。不要一开始就怀疑是代码 bug很多“诡异”行为就是连接资源被耗尽了。5.5 私钥导出后转币失败验签一步不能省现象扫到了一个命中地址导出了私钥但转账时签名一直报错。原因私钥和地址不匹配。这种情况通常是因为路径写错但地址比对时恰好走的是另一个错误路径导致你导出的私钥根本不是目标地址的私钥。代码不报错因为私钥本身就是任意合法数字它能生成一个地址但不是你想要的那个。解决转账之前用私钥反推一次地址做一次硬校验。Credentials check Credentials.create(privateKey); if (!check.getAddress().equalsIgnoreCase(targetAddress)) { throw new IllegalStateException(私钥与地址不匹配路径可能设置错误); }这一步成本极低但能把 5.1 的遗留问题彻底拦截在转账之前。我习惯把这行校验写进扫描器命中之后的处理流程里作为最后的保险。6. 让扫描器算得明白向量校验、bound 参数与断点续跑6.1 用固定熵校准全链路随机生成的助记词没法判断推导对不对因为每次都不一样。校准的方法是改用固定熵让结果可以复现把SecureRandom换成一个固定 16 字节数组跑一次把输出的助记词、地址记下来拿去和硬件钱包、MetaMask 或任何一个你信任的工具核对。一致说明这段链路是通的不一致直接带着固定结果去查资料比对着随机结果猜要快得多。// 固定熵用于校准不要在生产扫描里用它 byte[] fixedEntropy new byte[16]; // 全 0 String mnemonic MnemonicUtils.generateMnemonic(fixedEntropy);全零熵生成的助记词是确定的这个值在 BIP39 测试向量里也有对应的地址。你不一定记得住那个地址但可以用它做断言本地跑一次拿到结果再和手头钱包核对之后就把它当成回归用例每次改动代码后跑一遍确保推导链路没有被改坏。6.2 用 bound 参数验证扫描正确性扫描器的正确性验证有个笨办法但很管用把 bound 设成 1命中的日志文件清空跑一次。正常情况下不会命中任何地址但如果日志文件里出现了内容说明目标地址列表或者路径设置有 bug很可能你扫出来的地址和目标地址是同一个。然后再做一次“必然命中”的测试拿你自己钱包里某个真实地址把对应的助记词预先插进一个固定的输入文件作为扫描器测试用例。跑完检查 hit.csv 里有没有记录。我习惯把这两步写成一个 JUnit 测试类第一个用例断言“固定熵生成固定地址”第二个用例断言“手动插入的助记词能被扫描器发现”。以后改代码跑一遍测试就知道有没有改坏。6.3 扫描进度与存档进程崩溃不白跑随机扫描没有断点可续——每次生成的熵都是独立的跑过的一万次对下一次没有任何记忆。要让扫描可断点续跑必须把随机熵换成确定性序列用一个计数器作为熵的来源每次循环让计数器自增。这样第 N 次扫描用的熵是确定的崩溃后只要记录下 N下次从 N 继续就能完全跳过已扫描的部分。long counter startCounter; // 从存档恢复 for (; counter bound; counter) { byte[] entropy new byte[16]; // 前 8 字节放计数器后 8 字节补固定值熵仍然合法 for (int j 0; j 8; j) { entropy[j] (byte) (counter (j * 8)); } // 后续推导逻辑不变 }这个技巧牺牲了一部分随机性换来精确的断点续跑能力。在恢复自己钱包的场景里这比“每次随机碰运气”实用得多——你可以分批次扫每批次记录进度随时停下来换机器继续。我只在受控的恢复工具里这么用散落在目标列表里的地址是自己的计数器从 0 开始扫到 bound 就停。做到这一步这套工具已经从“碰运气”变成“算得明白”了推导链路有向量校准兜底扫描结果有命中测试兜底长任务有断点存档兜底。我自己的习惯是每加一个功能就重跑一遍固定熵测试确保没把链路改坏出错时先打印助记词、路径、目标地址这三样再做判断。希望帮到你。本文还有配套的精品资源点击获取