美文网首页Blockchain(区块链)
比特币学习5-HD钱包生成源码解读

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

作者: 梦幻艾斯 | 来源:发表于2018-05-16 11:01 被阅读614次

    前言

    本文所涉及的源码为bitcoinj-v0.14.7
    原文地址:https://www.jianshu.com/p/679e237e5bac
    作者:梦幻艾斯
    备注:欢迎转载,请保留原文地址。

    本文是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<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、在分析代码之前先介绍下几个关键类:

    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<DeterministicKeyChain>来间接管理两者中的ECKey。
    10. ChildNumber --key树中该层级的索引,bip32协议中的i
    11. ImmutableList<ChildNumber> -- 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<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做的工作:

    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<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做的工作。

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

    结语

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

    相关文章

      网友评论

        本文标题:比特币学习5-HD钱包生成源码解读

        本文链接:https://www.haomeiwen.com/subject/fyqpdftx.html