简介:这是一份基于Java web3j的以太坊助记词地址生成与余额查询工程,面向区块链技术学习者、数字货币安全研究者及需要理解助记词派生机制的开发者。工程支持直连自建或免费以太坊节点,依据助记词生成规则进行部分反推判断,将原本约4.8亿种单词组合缩减至0.3亿种,压缩约16倍,并自动生成对应地址、与节点交互查询余额并记录结果,可用于研究助记词遍历与地址派生流程。压缩包共53个文件,约19.79MB,以29个jar依赖库为主,涵盖web3j、bitcoinj、spongycastle等区块链与加密组件,另有7个java源码、7个class编译文件及properties、cofig等配置与日志文件,工程结构完整可直接导入运行。目前已有4183人学习下载,适合希望深入理解以太坊地址生成、节点交互与批量查询实现细节的读者参考。
1. 用 web3j 从助记词推导地址:先搞清楚「硬破解」到底在做什么
很多人第一次看到「java web3j 助记词生成地址 直连以太坊节点 查余额」这串词,脑子里浮现的画面是:拿到一串助记词,程序自动算出地址,连上以太坊节点,余额一查就出来了。前半段没错,后半段有个致命误解——你只能查自己知道私钥的地址余额,无法通过余额反推私钥。所谓「硬破解」,在工程语境里指的是「用已知助记词批量派生地址、批量查询链上状态」,而不是暴力穷举别人的钱包。这两件事的技术难度差了十几个数量级。
这篇文章面向的是有 Java 基础、想用 web3j 做链上数据查询或钱包工具开发的工程师。我会从 BIP39/BIP32/BIP44 的派生逻辑讲起,然后给出可直接运行的 Maven 依赖、助记词转地址的完整代码、直连以太坊节点的配置方式、余额查询的批量实现,最后把我在实际项目里踩过的坑一条条列出来。读完你应该能自己搭一个「输入助记词 → 输出多链地址 → 查余额」的本地工具,也能清楚知道哪些事能做、哪些事在数学上就不可能。
需要提前说清楚边界:本文所有操作都基于你自己持有的助记词或测试助记词,用于钱包管理、地址簿生成、空投筛查等合法场景。任何试图遍历他人助记词空间的行为,在 2^128 量级的搜索空间面前没有工程意义,也不在讨论范围内。
2. 助记词到地址的派生链路:BIP39、BIP32、BIP44 各自管什么
2.1 三个标准的分工与衔接
助记词生成地址不是一步完成的,中间要经过三层转换。BIP39负责「人类可读的助记词 ↔ 二进制种子」:它定义了一套 2048 个单词的词表,把 128~256 位熵编码成 12~24 个单词,同时支持一个可选的 passphrase(常被称为「第 13 个词」)。BIP32负责「种子 → 主密钥 → 子密钥」:它定义了分层确定性钱包(HD Wallet),用 HMAC-SHA512 从种子推出主私钥和链码,再通过派生路径生成任意层级的子密钥。BIP44负责「路径命名规范」:它规定了m / purpose' / coin_type' / account' / change / address_index这个五层结构,让不同币种、不同账户、不同地址索引有统一的寻址方式。
以太坊在 BIP44 里的 coin_type 是 60,所以最常见的派生路径是m/44'/60'/0'/0/0。这里每个带'的层级表示硬化派生(hardened derivation),不带'的是普通派生。硬化派生的子私钥无法从父公钥推出,安全性更高;普通派生允许只拿父公钥扩展出子公钥,适合观察钱包场景。
web3j 内部用的是 bitcoinj 的 BIP39/BIP32 实现,加上自己的Keys和Sign工具类完成 secp256k1 公钥推导和 Keccak-256 地址计算。理解这条链路之后,你就能明白为什么同一个助记词在不同钱包里可能生成不同地址——大概率是派生路径不一致。
2.2 派生路径差异导致的「地址对不上」
这是新手最容易翻车的地方。同一个助记词,MetaMask 默认用m/44'/60'/0'/0/0,但有些老钱包用m/44'/60'/0'/0(少一层),还有些硬件钱包默认从m/44'/60'/0'/0/0开始但账户索引从 0 递增的方式不同。更极端的情况是某些链用m/44'/60'/0'/0/0之外的 coin_type,比如 BSC 虽然兼容 EVM 但有些工具会用m/44'/60'以外的路径。
我的做法是:在代码里把派生路径做成可配置参数,默认m/44'/60'/0'/0/0,同时提供一个「扫描前 N 个地址」的功能,把address_index从 0 遍历到 N-1,这样即使不确定对方用的是哪个索引,也能覆盖到。下面这张表是我整理的主流场景路径对照:
| 场景 | 派生路径 | 说明 |
|---|---|---|
| MetaMask / 主流 EVM 钱包 | m/44'/60'/0'/0/0 | 第一个账户第一个地址 |
| 多账户钱包第 2 个账户 | m/44'/60'/1'/0/0 | account 层递增 |
| 同一账户第 2 个地址 | m/44'/60'/0'/0/1 | address_index 递增 |
| 部分硬件钱包 | m/44'/60'/0'/0 | 少一层,需兼容 |
| 测试用 | m/44'/1'/0'/0/0 | coin_type=1 是以太坊测试网旧规范 |
提示:如果你拿到的地址和预期对不上,第一件事就是检查派生路径,而不是怀疑助记词错了。我见过太多人在这上面耗半天。
3. 用 web3j 跑通助记词转地址:Maven 依赖与最小可运行代码
3.1 依赖配置与版本选择
web3j 的版本迭代比较快,核心包core包含了 BIP39/BIP32 和 secp256k1 的实现。我一般用 4.9.x 以上的稳定版,太老的版本在 JDK 17 上会有模块化相关的警告甚至报错。Maven 依赖如下:
<dependency> <groupId>org.web3j</groupId> <artifactId>core</artifactId> <version>4.9.8</version> </dependency> <dependency> <groupId>org.web3j</groupId> <artifactId>crypto</artifactId> <version>4.9.8</version> </dependency>如果你只需要助记词转地址、不涉及合约调用,crypto包其实就够了,它体积更小。但既然标题里要「直连以太坊节点查余额」,core里的HttpService和Web3j类是必须的,所以两个都加上。JDK 版本建议 11 或 17,8 也能跑但部分新特性用不了。
3.2 助记词转地址的完整代码
下面这段代码是我在实际工具里用的核心逻辑,做了封装,支持指定派生路径和地址索引:
import org.web3j.crypto.*; import org.web3j.crypto.MnemonicUtils; import java.math.BigInteger; public class MnemonicToAddress { /** * 从助记词派生以太坊地址 * @param mnemonic 12/15/18/21/24 个单词的助记词,空格分隔 * @param passphrase BIP39 可选密码,没有就传空字符串 * @param derivePath 派生路径,如 m/44'/60'/0'/0/0 * @return 地址(0x 开头)和私钥 */ public static DerivedKey derive(String mnemonic, String passphrase, String derivePath) { // 1. 助记词 + passphrase 生成 64 字节种子 byte[] seed = MnemonicUtils.generateSeed(mnemonic, passphrase); // 2. 从种子构建 BIP32 主密钥 Bip32ECKeyPair masterKeyPair = Bip32ECKeyPair.generateKeyPair(seed); // 3. 解析派生路径并派生出子密钥 int[] path = parsePath(derivePath); Bip32ECKeyPair childKeyPair = Bip32ECKeyPair.deriveKeyPair(masterKeyPair, path); // 4. 从子密钥拿到私钥和公钥 BigInteger privateKey = childKeyPair.getPrivateKey(); BigInteger publicKey = childKeyPair.getPublicKey(); // 5. 公钥做 Keccak-256,取后 20 字节作为地址 String address = "0x" + Keys.getAddress(publicKey); return new DerivedKey(address, privateKey.toString(16)); } /** * 把 m/44'/60'/0'/0/0 解析成 Bip32ECKeyPair 需要的 int 数组 * 带 ' 的层级最高位设为 1(硬化标志) */ private static int[] parsePath(String path) { String[] parts = path.replace("m/", "").split("/"); int[] result = new int[parts.length]; for (int i = 0; i < parts.length; i++) { String p = parts[i]; if (p.endsWith("'")) { // 硬化派生:索引 + 0x80000000 result[i] = Integer.parseInt(p.substring(0, p.length() - 1)) | 0x80000000; } else { result[i] = Integer.parseInt(p); } } return result; } public static class DerivedKey { public final String address; public final String privateKeyHex; public DerivedKey(String address, String privateKeyHex) { this.address = address; this.privateKeyHex = privateKeyHex; } } public static void main(String[] args) { // 用测试助记词,切勿用真实资产助记词在联网环境跑 String mnemonic = "test test test test test test test test test test test junk"; DerivedKey key = derive(mnemonic, "", "m/44'/60'/0'/0/0"); System.out.println("地址: " + key.address); System.out.println("私钥: " + key.privateKeyHex); } }逻辑说明:MnemonicUtils.generateSeed内部走的是 PBKDF2-HMAC-SHA512,迭代 2048 次,这是 BIP39 规定的参数,不要改。Bip32ECKeyPair.generateKeyPair用种子算出主密钥,deriveKeyPair按路径逐层派生。parsePath里那个| 0x80000000是硬化派生的标志位,BIP32 规定索引最高位为 1 表示硬化,这个细节如果写错,派生出来的地址会完全不对。
参数说明:passphrase大多数钱包传空字符串,但如果你在创建钱包时设了额外密码,必须传对,否则地址完全不同。derivePath建议做成配置项,不要硬编码。address_index就是路径最后一位,批量生成时循环递增即可。
3.3 批量派生多个地址
实际工具里往往需要一次生成一批地址,比如做空投筛查或者地址簿。把上面的derive包一层循环:
public static List<DerivedKey> deriveBatch(String mnemonic, String passphrase, String basePath, int count) { List<DerivedKey> list = new ArrayList<>(); for (int i = 0; i < count; i++) { // basePath 形如 m/44'/60'/0'/0,拼上索引 String path = basePath + "/" + i; list.add(derive(mnemonic, passphrase, path)); } return list; }count一般设 20 就够覆盖大多数钱包的默认展示数量,MetaMask 默认展示 1 个但内部会预派生 20 个。如果你要扫描「这个助记词下有没有资产」,建议至少扫 50 个索引,因为有些用户手动点过「创建新账户」,索引会往后走。
4. 直连以太坊节点查余额:HttpService 配置与批量查询实现
4.1 节点接入方式与选型
web3j 连节点有三种方式:HttpService(HTTP JSON-RPC)、WebSocketService(WS)、IpcService(本地 IPC 文件)。查余额这种低频只读操作,HTTP 就够了,稳定且好排查。节点来源可以是自己跑的 Geth/Erigon,也可以是第三方 RPC 服务商。自己跑全节点同步慢、占资源,但数据可信;第三方 RPC 快但要注意限流和隐私。
配置Web3j实例的代码:
import org.web3j.protocol.Web3j; import org.web3j.protocol.http.HttpService; // 替换成你自己的节点地址 String rpcUrl = "https://your-ethereum-node.example.com"; Web3j web3j = Web3j.build(new HttpService(rpcUrl)); // 查询链 ID 验证连通性 long chainId = web3j.ethChainId().send().getChainId().longValue(); System.out.println("已连接,chainId = " + chainId);HttpService默认超时是 10 秒,批量查询时建议调大,构造时可以传OkHttpClient自定义超时。如果你用的是第三方 RPC,注意它的请求频率限制,免费档通常每秒几次,批量查几百个地址会被限流。
4.2 单地址余额查询与单位换算
以太坊的eth_getBalance返回的是 wei,单位是 10^18。web3j 提供了Convert.fromWei做换算:
import org.web3j.protocol.core.methods.response.EthGetBalance; import org.web3j.utils.Convert; import java.math.BigDecimal; public static BigDecimal getBalance(Web3j web3j, String address) throws Exception { EthGetBalance balance = web3j.ethGetBalance( address, DefaultBlockParameterName.LATEST // 最新区块 ).send(); BigInteger wei = balance.getBalance(); // 转成 ether,保留 6 位小数方便看 return Convert.fromWei(new BigDecimal(wei), Convert.Unit.ETHER); }DefaultBlockParameterName.LATEST表示查最新区块的状态,也可以传EARLIEST或具体区块号。注意ethGetBalance对不存在的地址返回 0,不会报错,所以你不能靠返回值判断地址是否有效。
4.3 批量查询与并发控制
批量查余额如果串行发请求,100 个地址可能要几十秒。用线程池并发能快很多,但要控制并发数避免被 RPC 限流:
import java.util.concurrent.*; public static Map<String, BigDecimal> batchQuery( Web3j web3j, List<String> addresses, int concurrency) throws Exception { ExecutorService pool = Executors.newFixedThreadPool(concurrency); Map<String, BigDecimal> result = new ConcurrentHashMap<>(); List<Future<?>> futures = new ArrayList<>(); for (String addr : addresses) { futures.add(pool.submit(() -> { try { BigDecimal bal = getBalance(web3j, addr); result.put(addr, bal); } catch (Exception e) { // 单个失败不影响整体,记录后继续 result.put(addr, BigDecimal.valueOf(-1)); } })); } for (Future<?> f : futures) { f.get(30, TimeUnit.SECONDS); } pool.shutdown(); return result; }并发数我一般设 5~10,第三方 RPC 免费档设 3 比较稳。f.get加超时是防止某个请求卡死拖垮整个批次。失败时记 -1 而不是抛异常,这样你能区分「余额为 0」和「查询失败」。
注意:批量查询会暴露你查询的地址列表给 RPC 服务商。如果地址涉及隐私,建议用自建节点。这是很多人忽略的隐私问题。
5. 避坑与排查:助记词派生和余额查询里最容易翻车的 5 个点
5.1 地址对不上:九成是派生路径或 passphrase 的问题
现象:同一个助记词,代码算出的地址和 MetaMask 显示的不一样。原因:要么派生路径不一致(比如代码用了m/44'/60'/0'/0/0但钱包用的是m/44'/60'/0'/0),要么创建钱包时设了 passphrase 但代码里传了空字符串。解决:先用一个已知的测试助记词(比如test test ... junk)对照 MetaMask 导入后的地址,确认路径和 passphrase 都对,再换真实助记词。MetaMask 导入时可以选「导入现有钱包」,能看到它用的路径。
5.2 中文助记词或多余空格导致解析失败
现象:MnemonicUtils.generateSeed抛IllegalArgumentException或算出的地址完全不对。原因:助记词里混入了中文空格、全角空格、换行符,或者单词之间用了多个空格。BIP39 要求单词用单个半角空格分隔,且单词必须在词表里。解决:入库前先做规范化——mnemonic.trim().replaceAll("\\s+", " "),然后校验每个单词是否在 BIP39 英文词表里。web3j 的MnemonicUtils.validateMnemonic可以做校验,但它对大小写敏感,建议先转小写。
5.3 RPC 限流导致批量查询大面积失败
现象:批量查 100 个地址,前 20 个成功,后面全返回 -1 或超时。原因:第三方 RPC 服务商对免费档有 QPS 限制,并发一高就返回 429。解决:把并发数降到 3 以下,或者在每次请求之间加 100~200ms 延迟。更稳的做法是用信号量控制速率,而不是靠线程池大小。如果长期要批量查,建议自建节点或买付费 RPC。
5.4 私钥泄露:日志和异常堆栈是重灾区
现象:程序跑完发现私钥被打印到了日志文件或控制台。原因:调试时随手System.out.println(privateKey),或者异常堆栈里带了包含私钥的对象。解决:生产代码里永远不要打印私钥,DerivedKey的toString要重写并脱敏。日志框架配置里对包含privateKey的字段做过滤。我自己的习惯是私钥只在内存里传递,用完立即置空,绝不落盘。
5.5 把「查余额」误解成「能破解」
现象:有人以为写个程序遍历助记词就能找到有余额的地址。原因:对 BIP39 的搜索空间没有概念。12 个单词的助记词对应 128 位熵,搜索空间是 2^128,即使每秒试 10 亿个,也要 10^22 年。解决:认清工程边界。这个技术栈的正确用途是管理自己的钱包、批量生成地址、做空投筛查、构建地址簿,而不是「硬破解」。把精力放在合法场景上,收益更实在。
6. 进阶技巧:用本地缓存和离线派生把查询效率提上去
批量查询场景下,最耗时的往往不是链上查询本身,而是重复的密钥派生。每次derive都要跑一遍 PBKDF2(2048 次迭代),1000 个地址就是 1000 次,累计能到几秒。我的优化做法是:主密钥只派生一次,后续子密钥派生复用主密钥。Bip32ECKeyPair.generateKeyPair(seed)的结果缓存起来,循环里只调deriveKeyPair,能省掉 90% 的计算。
另一个技巧是离线派生 + 在线查询分离。先把所有地址在本地算好,存到一个List<String>里,再统一发查询请求。这样即使 RPC 挂了,地址生成部分也不受影响,重试时不用重新派生。我一般会把地址列表和对应的派生路径索引一起存成 JSON,方便后续对账。
验证派生结果是否正确,有个简单办法:用同一个助记词在 MetaMask 里导入,对比前 5 个地址。如果全对,说明路径和 passphrase 都没问题。如果只有第一个对、后面不对,检查address_index的递增逻辑。如果全不对,检查 passphrase 和路径的硬化标志。
最后说个我自己的习惯:任何涉及私钥的代码,我都会先在测试网跑一遍,用m/44'/1'/0'/0/0这个测试路径,确认整个链路通了再切主网。测试助记词用公开的test test ... junk,绝不混用真实助记词。这个习惯帮我避免过至少两次因为路径写错导致的资产误判。希望帮到你。
本文还有配套的精品资源,点击获取