HD钱包生成源码解读

比特币学习5-HD钱包生成源码解读


前言


本文是bip32、bip39、bip44的源码实现,主要描述了HD钱包的生成过程。
HD钱包生成流程:

  1. 随机数生成助记词
  2. 助记词生成种子
  3. 种子生成主私钥

生成助记词

下面是创建一个HD钱包的方法.

  1. 首先选择网络
  2. 然后生成助记词
  3. 最后通过助记词生成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 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 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 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、在分析代码之前先介绍下几个关键类:

  1. ECPoint --椭圆上的点,公钥在java中就是一个ECPoint实例。
  2. BigInteger --大int类,私钥在java中就是一个BigInteger实例
  3. ECKey --密钥对,包含一个公钥和一个私钥,私钥可以为空。
  4. DeterministicKey --ECKey的子类,多了链码、深度、子节点路径、父节点等属性。可以把它当成一个扩展密钥。
  5. HDKeyDerivation --HDkey的工具类,用于生成主密钥、推导子扩展密钥等。
  6. BasicKeyChain --ECKey的集合,里面的元素没有相关性。
  7. DeterministicHierarchy --维护一个DeterministicKey树
  8. DeterministicKeyChain --对DeterministicHierarchy做业务操作,比如设置某个DeterministicKey已经使用。
  9. KeyChainGroup --通过管理BasicKeyChain和List来间接管理两者中的ECKey。
  10. ChildNumber --key树中该层级的索引,bip32协议中的i
  11. ImmutableList -- key树中该层级的路径,如:m / 44' / 0' / 0'
  12. 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做的工作:

  1. 生成了主密钥rootKey。
  2. 创建了用于存储树节点(DeterministicKey)的basicKeyChain.
  3. 创建了树结构hierarchy。
  4. 创建了外部链节点externalParentKey,内部链节点internalParentKey
  5. 将主节点、外部节点、内部节点都放到了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 chains,
                          @Nullable EnumMap currentKeys, @Nullable KeyCrypter crypter) {
        this.params = params;
        //新建一个BasicKeyChain
        this.basic = basicKeyChain == null ? new BasicKeyChain() : basicKeyChain;
        
        //新建一个list并把上一步生成的DeterministicKeyChain添加进去
        this.chains = new LinkedList(checkNotNull(chains));
        
        //this.keyCrypter = null;
        this.keyCrypter = crypter;
        
        //新建当前节点
        this.currentKeys = currentKeys == null
                ? new EnumMap(KeyChain.KeyPurpose.class)
                : currentKeys;
                
        //创建当前地址
        this.currentAddresses = new EnumMap(KeyChain.KeyPurpose.class);
        
        //循环执行chains里面元素的maybeLookAheadScripts方法,目前看DeterministicKeyChain的maybeLookAheadScripts方法是空的
        //也就是说这里什么都没做
        maybeLookaheadScripts();
        
        //如果是多重签名,按照多重签名的方式生成地址
        if (isMarried()) {
            for (Map.Entry entry : this.currentKeys.entrySet()) {
                Address address = makeP2SHOutputScript(entry.getValue(), getActiveKeyChain()).getToAddress(params);
                currentAddresses.put(entry.getKey(), address);
            }
        }
    }

总结一下创建KeyChainGroup做的工作:

  1. 新建一个BasicKeyChain
  2. 新建一个list并把上一步生成的DeterministicKeyChain添加进去
  3. 新建当前节点
  4. 创建当前地址
    注: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();
        //已花费交易
        spent = new HashMap();
        //进行中的交易
        pending = new HashMap();
        //失败的交易
        dead = new HashMap();
        
        //交易列表
        transactions = new HashMap();
        
        //钱包扩展
        extensions = new HashMap();
        // Use a linked hash map to ensure ordering of event listeners is correct.
        confidenceChanged = new LinkedHashMap();
        
        //签名列表,发送交易时用到
        signers = new ArrayList();
        //添加一个本地签名
        addTransactionSigner(new LocalTransactionSigner());
        
        //创建块确认监听
        createTransientState();
    }
    
    //创建块确认监听,收到交易或发送交易后用到
      private void createTransientState() {
        ignoreNextNewBlock = new HashSet();
        txConfidenceListener = new TransactionConfidence.Listener() {
            @Override
            public void onConfidenceChanged(TransactionConfidence confidence, TransactionConfidence.Listener.ChangeReason reason) {
                略
                }
            }
        };
        acceptRiskyTransactions = false;
    }

总结一下创建Wallet做的工作。

  1. 给context、param、keyChainGroup赋值
  2. 创建观察脚本列表
  3. 创建各种交易列表
  4. 创建签名列表并把LocalTransactionSigner添加进去
  5. 创建块确认监听方法。

结语

以上就是一个钱包从助记词选择到生成种子,再由种子生成钱包的过程。
当然这只是刚刚开始,后面还有很多工作。比如导入新钱包时如何同步交易、如何发现账户。
收到交易时钱包如何工作、钱包如何发送交易?后续内容挺多的,敬请期待

你可能感兴趣的:(比特币)