前言
本文所涉及的源码为bitcoinj-v0.14.7
原文地址:https://www.jianshu.com/p/679e237e5bac
作者:梦幻艾斯
备注:欢迎转载,请保留原文地址。
本文是bip32、bip39、bip44的源码实现,主要描述了HD钱包的生成过程。
HD钱包生成流程:
- 随机数生成助记词
- 助记词生成种子
- 种子生成主私钥
生成助记词
下面是创建一个HD钱包的方法.
- 首先选择网络
- 然后生成助记词
- 最后通过助记词生成HD钱包
我们跟下代码来看看生成助记词的具体实现。
NetworkParameters params = TestNet3Params.get();
DeterministicSeed seed = new DeterministicSeed(new SecureRandom(),128,"password",Utils.currentTimeSeconds());
Wallet wallet = Wallet.fromSeed(params,seed);
DeterministicSeed的构造方法:
public DeterministicSeed(SecureRandom random, int bits, String passphrase, long creationTimeSeconds) {
this(getEntropy(random, bits), checkNotNull(passphrase), creationTimeSeconds);
}
先来看看getEntropy函数
private static byte[] getEntropy(SecureRandom random, int bits) {
checkArgument(bits <= MAX_SEED_ENTROPY_BITS, "requested entropy size too large");
byte[] seed = new byte[bits / 8];
random.nextBytes(seed);
return seed;
}
可以看出通过getEntropy函数得到一个byte数组,然后作为参数传给构造方法2
DeterministicSeed的构造方法2:
public DeterministicSeed(byte[] entropy, String passphrase, long creationTimeSeconds) {
//检查参数的正确性
checkArgument(entropy.length % 4 == 0, "entropy size in bits not divisible by 32");
checkArgument(entropy.length * 8 >= DEFAULT_SEED_ENTROPY_BITS, "entropy size too small");
checkNotNull(passphrase);
try {
//生成助记词
this.mnemonicCode = MnemonicCode.INSTANCE.toMnemonic(entropy);
} catch (MnemonicException.MnemonicLengthException e) {
// cannot happen
throw new RuntimeException(e);
}
//通过助记词生成种子,详情看“通过助记词生成种子”
this.seed = MnemonicCode.toSeed(mnemonicCode, passphrase);
this.encryptedMnemonicCode = null;
this.creationTimeSeconds = creationTimeSeconds;
}
我们来看看MnemonicCode.INSTANCE.toMnemonic方法是如何从随机生成的byte数组得出助记词的
代码位置MnemonicCode.java
/**
* entropy为上面通过SecureRandom生成的随机数组
**/
public List<String> toMnemonic(byte[] entropy) throws MnemonicException.MnemonicLengthException {
//为了减少字数删来检查参数的代码
//计算entropyhash作为后面的checksum
byte[] hash = Sha256Hash.hash(entropy);
//将hash转换成二进制,true为1,false为0。详情请看bytesToBits函数的解析
boolean[] hashBits = bytesToBits(hash);
//将随机数组转换成二进制
boolean[] entropyBits = bytesToBits(entropy);
//checksum长度
int checksumLengthBits = entropyBits.length / 32;
// 将entropyBits和checksum加起来,相当于BIP39中的ENT+CS
boolean[] concatBits = new boolean[entropyBits.length + checksumLengthBits];
System.arraycopy(entropyBits, 0, concatBits, 0, entropyBits.length);
System.arraycopy(hashBits, 0, concatBits, entropyBits.length, checksumLengthBits);
/**
*this.wordList是助记词列表。
*
**/
ArrayList<String> words = new ArrayList<>();
//助记词个数
int nwords = concatBits.length / 11;
for (int i = 0; i < nwords; ++i) {
int index = 0;
for (int j = 0; j < 11; ++j) {
//java中int是由32位二进制组成,index左移1位,如果concatBits对应的位为true则将index对应的位设置位1
index <<= 1;
if (concatBits[(i * 11) + j])
index |= 0x1;
}
//根据索引从助记词列表中获取单词并添加到words
words.add(this.wordList.get(index));
}
//得到的助记词
return words;
}
bytesToBits函数,代码位置MnemonicCode.java
private static boolean[] bytesToBits(byte[] data) {
//java中byte使用8位二进制表示
boolean[] bits = new boolean[data.length * 8];
for (int i = 0; i < data.length; ++i)
//循环data[i]中的没一位,如果data[i]的j位为1则bits[(i * 8) + j]=true否则为false
for (int j = 0; j < 8; ++j)
bits[(i * 8) + j] = (data[i] & (1 << (7 - j))) != 0;
return bits;
}
通过助记词生成种子
上一节通过随机数获得助记词后,使用MnemonicCode.toSeed可以推导出种子。
我们来看看这个方法的具体实现
public DeterministicSeed(byte[] entropy, String passphrase, long creationTimeSeconds) {
...
this.seed = MnemonicCode.toSeed(mnemonicCode, passphrase);
...
}
MnemonicCode.toSeed函数,位置MnemonicCode.java
public static byte[] toSeed(List<String> words, String passphrase) {
checkNotNull(passphrase, "A null passphrase is not allowed.");
// To create binary seed from mnemonic, we use PBKDF2 function
// with mnemonic sentence (in UTF-8) used as a password and
// string "mnemonic" + passphrase (again in UTF-8) used as a
// salt. Iteration count is set to 4096 and HMAC-SHA512 is
// used as a pseudo-random function. Desired length of the
// derived key is 512 bits (= 64 bytes).
//将助记词连接起来,以空格作为分隔符。pass格式:"aa bb cc dd ..."
String pass = Utils.SPACE_JOINER.join(words);
String salt = "mnemonic" + passphrase;
final Stopwatch watch = Stopwatch.createStarted();
//使用PBKDF2SHA512生成64位的种子
byte[] seed = PBKDF2SHA512.derive(pass, salt, PBKDF2_ROUNDS, 64);
watch.stop();
log.info("PBKDF2 took {}", watch);
return seed;
}
从种子生成HD钱包
通过上面两个步骤我们得到了种子seed,现在通过这个seed生成钱包。我们来看看Wallet.fromSeed这个函数到底做了那些工作。
NetworkParameters params = TestNet3Params.get();
DeterministicSeed seed = new DeterministicSeed(new SecureRandom(),128,"password",Utils.currentTimeSeconds());
Wallet wallet = Wallet.fromSeed(params,seed);
1、在分析代码之前先介绍下几个关键类:
- ECPoint --椭圆上的点,公钥在java中就是一个ECPoint实例。
- BigInteger --大int类,私钥在java中就是一个BigInteger实例
- ECKey --密钥对,包含一个公钥和一个私钥,私钥可以为空。
- DeterministicKey --ECKey的子类,多了链码、深度、子节点路径、父节点等属性。可以把它当成一个扩展密钥。
- HDKeyDerivation --HDkey的工具类,用于生成主密钥、推导子扩展密钥等。
- BasicKeyChain --ECKey的集合,里面的元素没有相关性。
- DeterministicHierarchy --维护一个DeterministicKey树
- DeterministicKeyChain --对DeterministicHierarchy做业务操作,比如设置某个DeterministicKey已经使用。
- KeyChainGroup --通过管理BasicKeyChain和List<DeterministicKeyChain>来间接管理两者中的ECKey。
- ChildNumber --key树中该层级的索引,bip32协议中的i
- ImmutableList<ChildNumber> -- key树中该层级的路径,如:m / 44' / 0' / 0'
- KeyPurpose --定义在KeyChain.java中的一个枚举类。系统用这个类来标记节点类型
- RECEIVE_FUNDS 收款地址,从外部链中派生。
- CHANGE 找零地址,从内部链中派生。
- REFUND 退款地址,从外部链中派生。具体看BIP70协议。
- AUTHENTICATION 认证地址,从内部链中派生。
2、Wallet.fromSeed相关代码,创建Wallet1:
public static Wallet fromSeed(NetworkParameters params, DeterministicSeed seed) {
//创建一个KeyChainGroup实例
return new Wallet(params, new KeyChainGroup(params, seed));
}
public Wallet(NetworkParameters params, KeyChainGroup keyChainGroup) {
this(Context.getOrCreate(params), keyChainGroup);
}
private Wallet(Context context, KeyChainGroup keyChainGroup) {
创建wallet的具体方法,稍后分析
}
从上面代码我们知道。程序先创建了一个KeyChainGroup对象然后再创建wallet对象。我们先看看KeyChainGroup对象是怎么创建的,然后再分析wallet对象的创建过程。
3、创建KeyChainGroup1
KeyChainGroup创建代码,位置KeyChainGroup.java
public KeyChainGroup(NetworkParameters params, DeterministicSeed seed) {
this(params, null, ImmutableList.of(new DeterministicKeyChain(seed)), null, null);
}
4、创建DeterministicKeyChain
从上面的关键类介绍中我们知道KeyChainGroup只是包装了DeterministicKeyChain和BasicKeyChain。实际干活的还是它们两个。
我们来看下DeterministicKeyChain的创建代码,
private DeterministicHierarchy hierarchy;
@Nullable private DeterministicKey rootKey;
@Nullable private DeterministicSeed seed;
private final BasicKeyChain basicKeyChain;
protected DeterministicKeyChain(DeterministicSeed seed) {
this(seed, null);
}
protected DeterministicKeyChain(DeterministicSeed seed, @Nullable KeyCrypter crypter) {
this.seed = seed;
//这里使用BasicKeyChain是为了方便按照hash查找DeterministicKey
//所有产生的DeterministicKey都会添加到这个basicKeyChain中
basicKeyChain = new BasicKeyChain(crypter);
//因为没有对seed加密,所以会进入这段代码
if (!seed.isEncrypted()) {
//根据bip32的公式生成
rootKey = HDKeyDerivation.createMasterPrivateKey(checkNotNull(seed.getSeedBytes()));
//设置创建实际
rootKey.setCreationTimeSeconds(seed.getCreationTimeSeconds());
//将rootKey添加到basicKeyChain
addToBasicChain(rootKey);
//创建树结构
hierarchy = new DeterministicHierarchy(rootKey);
//将hierarchy中第一层子节点都添加到basicKeyChain
for (int i = 1; i <= getAccountPath().size(); i++) {
addToBasicChain(hierarchy.get(getAccountPath().subList(0, i), false, true));
}
//初始化这个树结构
initializeHierarchyUnencrypted(rootKey);
}
}
//初始化这个树结构
private void initializeHierarchyUnencrypted(DeterministicKey baseKey) {
//创建外部链
externalParentKey = hierarchy.deriveChild(getAccountPath(), false, false, ChildNumber.ZERO);
//创建内部链
internalParentKey = hierarchy.deriveChild(getAccountPath(), false, false, ChildNumber.ONE);
//将内部链和外部链添加到basicKeyChain
addToBasicChain(externalParentKey);
addToBasicChain(internalParentKey);
}
总结一下创建DeterministicKeyChain做的工作:
- 生成了主密钥rootKey。
- 创建了用于存储树节点(DeterministicKey)的basicKeyChain.
- 创建了树结构hierarchy。
- 创建了外部链节点externalParentKey,内部链节点internalParentKey
- 将主节点、外部节点、内部节点都放到了basicKeyChain
5、创建KeyChainGroup2
通过创建KeyChainGroup1代码我们知道。
创建DeterministicKeyChain成功之后,会把这个DeterministicKeyChain作为参数继续传教KeyChainGroup
下面是KeyChainGroup的另外一个构造函数。
//创建KeyChainGroup1代码
public KeyChainGroup(NetworkParameters params, DeterministicSeed seed) {
this(params, null, ImmutableList.of(new DeterministicKeyChain(seed)), null, null);
}
private KeyChainGroup(NetworkParameters params, @Nullable BasicKeyChain basicKeyChain, List<DeterministicKeyChain> chains,
@Nullable EnumMap<KeyChain.KeyPurpose, DeterministicKey> currentKeys, @Nullable KeyCrypter crypter) {
this.params = params;
//新建一个BasicKeyChain
this.basic = basicKeyChain == null ? new BasicKeyChain() : basicKeyChain;
//新建一个list并把上一步生成的DeterministicKeyChain添加进去
this.chains = new LinkedList<DeterministicKeyChain>(checkNotNull(chains));
//this.keyCrypter = null;
this.keyCrypter = crypter;
//新建当前节点
this.currentKeys = currentKeys == null
? new EnumMap<KeyChain.KeyPurpose, DeterministicKey>(KeyChain.KeyPurpose.class)
: currentKeys;
//创建当前地址
this.currentAddresses = new EnumMap<KeyChain.KeyPurpose, Address>(KeyChain.KeyPurpose.class);
//循环执行chains里面元素的maybeLookAheadScripts方法,目前看DeterministicKeyChain的maybeLookAheadScripts方法是空的
//也就是说这里什么都没做
maybeLookaheadScripts();
//如果是多重签名,按照多重签名的方式生成地址
if (isMarried()) {
for (Map.Entry<KeyChain.KeyPurpose, DeterministicKey> entry : this.currentKeys.entrySet()) {
Address address = makeP2SHOutputScript(entry.getValue(), getActiveKeyChain()).getToAddress(params);
currentAddresses.put(entry.getKey(), address);
}
}
}
总结一下创建KeyChainGroup做的工作:
- 新建一个BasicKeyChain
- 新建一个list并把上一步生成的DeterministicKeyChain添加进去
- 新建当前节点
- 创建当前地址
注:currentKeys用于普通地址,按照KeyPurpose分类可以理解为当前收款地址、当前找零地址、当前退款地址、当前认证地址。
currentAddresses用于多重签名地址。
currentAddresses对应的DeterministicKeyChain为MarriedKeyChain。
MarriedKeyChain是DeterministicKeyChain的一个子类。
创建wallet2
KeyChainGroup创建好后继续创建wallet
private Wallet(Context context, KeyChainGroup keyChainGroup) {
this.context = context;
this.params = context.getParams();
this.keyChainGroup = checkNotNull(keyChainGroup);
//单元测试,可以忽略
if (params.getId().equals(NetworkParameters.ID_UNITTESTNET))
this.keyChainGroup.setLookaheadSize(5); // Cut down excess computation for unit tests.
//查看keyChainGroup是否有ECKey,如果没有创建一个。按照我们的流程,是不会进入if里面的代码段
if (this.keyChainGroup.numKeys() == 0)
this.keyChainGroup.createAndActivateNewHDChain();
//观察列表,查看交易、余额等
watchedScripts = Sets.newHashSet();
//未花费交易
unspent = new HashMap<Sha256Hash, Transaction>();
//已花费交易
spent = new HashMap<Sha256Hash, Transaction>();
//进行中的交易
pending = new HashMap<Sha256Hash, Transaction>();
//失败的交易
dead = new HashMap<Sha256Hash, Transaction>();
//交易列表
transactions = new HashMap<Sha256Hash, Transaction>();
//钱包扩展
extensions = new HashMap<String, WalletExtension>();
// Use a linked hash map to ensure ordering of event listeners is correct.
confidenceChanged = new LinkedHashMap<Transaction, TransactionConfidence.Listener.ChangeReason>();
//签名列表,发送交易时用到
signers = new ArrayList<TransactionSigner>();
//添加一个本地签名
addTransactionSigner(new LocalTransactionSigner());
//创建块确认监听
createTransientState();
}
//创建块确认监听,收到交易或发送交易后用到
private void createTransientState() {
ignoreNextNewBlock = new HashSet<Sha256Hash>();
txConfidenceListener = new TransactionConfidence.Listener() {
@Override
public void onConfidenceChanged(TransactionConfidence confidence, TransactionConfidence.Listener.ChangeReason reason) {
略
}
}
};
acceptRiskyTransactions = false;
}
总结一下创建Wallet做的工作。
- 给context、param、keyChainGroup赋值
- 创建观察脚本列表
- 创建各种交易列表
- 创建签名列表并把LocalTransactionSigner添加进去
- 创建块确认监听方法。
结语
以上就是一个钱包从助记词选择到生成种子,再由种子生成钱包的过程。
当然这只是刚刚开始,后面还有很多工作。比如导入新钱包时如何同步交易、如何发现账户。
收到交易时钱包如何工作、钱包如何发送交易?后续内容挺多的,敬请期待。
网友评论