分层确定性钱包-以太坊创建钱包

时间:2024-04-02 16:29:27

基本概念

所有问题大体可以分为三类:区块链基本概念,钱包安全知识以及钱包转账交易

分层确定性钱包-以太坊创建钱包

区块链的基本特性

去中心化

因为整个网络没有中心统治者。系统依靠的是网络上多个参与者的公平约束,所以任意每几个节点的权利和义务都是均等的,而且每一个节点都会储存这个区块链上所有数据。即使该节点被损坏或遭受攻击,仍然不会对账簿造成任何威胁。

不可逆

区块链上的信息必须不可撤销,不能随意销毁。系统是开源的,整个系统都必须是公开透明的,因此某笔交易被全网广播以后,达到 6 个确认以上就成功记录在案了,且不可逆转不可撤销。注: imToken 是 12 个区块确认。

不可篡改

确保信息或合约无法伪造。账簿在某个人或某几人手上,造假的可能性就非常高,但每个人手里都有一本账簿,除非整个游戏里超过51%的人都更改某一笔账目,否则任何的篡改都是无效的,这也是集体维护和监督的优越性。

匿名性

各区块节点的身份信息不需要公告或验证, 信息传递可以匿名进行。举个简单的例子, 就是你在区块链上向一个钱包地址发起交易, 但是却无法知道这个地址背后确切对应的是那一个人, 或者你的私钥被某一个黑客盗窃了, 无法从一个钱包地址中得知黑客是谁。

以太坊基本概念

钱包地址

以0x开头的42位的哈希值 (16进制) 字符串

keystore

明文私钥通过加密算法加密过后的 JSON 格式的字符串, 一般以文件形式存储

助记词

12 (或者 15、18、21) 单词构成, 用户可以通过助记词导入钱包, 但反过来讲, 如果他人得到了你的助记词, 不需要任何密码就可以轻而易举的转移你的资产, 所以要妥善保管自己的助记词

明文私钥

64位的16进制哈希值字符串, 用一句话阐述明文私钥的重要性 “谁掌握了私钥, 谁就掌握了该钱包的使用权!” 同样, 如果他人得到了你的明文私钥, 不需要任何密码就可以轻而易举的转移你的资产

分层确定性钱包-以太坊创建钱包

通俗地去解释,以银行账户为类比,这些名词分别对应内容如下:

  • 钱包地址 = 银行卡号
  • 密码 = 银行卡密码
  • 私钥 = 银行卡号 + 银行卡密码
  • 助记词 = 银行卡号 + 银行卡密码
  • Keystore+ 密码 = 银行卡号 + 银行卡密码
  • Keystore ≠ 银行卡号

Note: 此小节大部分内容摘自imToken 测评通关攻略

BIP32,BIP39,BIP44

在创建钱包前,得先说下BIP32,BIP39,BIP44

BIP 全名是 Bitcoin Improvement Proposals,是提出 Bitcoin 的新功能或改进措施的文件。可由任何人提出,经过审核后公布在 bitcoin/bips 上。BIP 和 Bitcoin 的关系,就像是 RFC 之于 Internet。

而其中的 BIP32, BIP39, BIP44 共同定义了目前被广泛使用的 HD Wallet,包含其设计动机和理念、实作方式、实例等。

BIP32

定义 Hierarchical Deterministic wallet (简称 “HD Wallet”),是一个系统可以从单一个 seed 产生一树状结构储存多组 keypairs(私钥和公钥)。好处是可以方便的备份、转移到其他相容装置(因为都只需要 seed),以及分层的权限控制等

分层确定性钱包-以太坊创建钱包

BIP39

将 seed 用方便记忆和书写的单字表示。一般由 12 个单字组成,称为 mnemonic code(phrase),中文称为助记词或助记码。例如

1
rose rocket invest real refuse margin festival danger anger border idle brown

BIP44

基于 BIP32 的系统,赋予树状结构中的各层特殊的意义。让同一个 seed 可以支援多币种、多帐户等。各层定义如下:

1
m / purpose' / coin_type' / account' / change / address_index

其中的 purporse' 固定是 44',代表使用 BIP44。而 coin_type' 用来表示不同币种,例如 Bitcoin 就是 0',Ethereum 是 60'

Ethereum HD Wallet

Ethereum 的钱包目前均采用以上 Bitcoin HD Wallet 的架构,并订 coin_type' 为 60',可以在 ethereum/EIPs/issues 中看到相关的讨论。举例来说,在一个 Ethereum HD Wallet 中,第一个帐户(这里的帐户指 BIP44 中定义的 account')的第一组 keypair,其路径会是 m/44'/60'/0'/0/0

创建钱包

在了解BIP 后,我们开始以太坊钱包开发,创建的钱包的流程为:

  1. 随机生成一组助记词
  2. 生成 seed
  3. 生成 master key
  4. 生成 child key
  5. 我们取第一组child key即m/44'/60'/0'/0/0 得到私钥,keystore及地址

bitcoinj

这里我们需要配置开发环境,通常Android 开发以太坊钱包,会依赖两个库 bitcoinj , web3j

bitcoinj 主要用来实现BIP32和BIP44 , web3j 实现BIP39和钱包功能 .

1
2
implementation 'org.web3j:core:3.3.1-android'
implementation 'org.bitcoinj:bitcoinj-core:0.14.7'

不过web3j在生成助记词中需要使用到MnemonicUtils,但是有坑,加载助记词列表文件的方式在Android上不行,会导致Crash

1
2
3
4
5
6
7
8
9
private static List<String> populateWordList() {
       URL url = Thread.currentThread().getContextClassLoader()
               .getResource("en-mnemonic-word-list.txt");
       try {
           return readAllLines(url.toURI().getSchemeSpecificPart());
       } catch (Exception e) {
           return Collections.emptyList();
       }
   }

这是Java的加载方式,Android上需要做平台适配,需要将en-mnemonic-word-list.txt文件放到assets目录下加载.

以下笔者则使用了另一个BIP39的依赖库

1
implementation 'io.github.novacrypto:BIP39:0.1.9'

本篇在使用中还用到了RxJava2

1

1
2
3
4
5
6
7
8
9
10
11
private Flowable<String> mnemonics() {
        // 1. 生成一组随机的助记词
        StringBuilder sb = new StringBuilder();
        byte[] entropy = new byte[Words.TWELVE.byteLength()];
        new SecureRandom().nextBytes(entropy);
        new MnemonicGenerator(English.INSTANCE)
                .createMnemonic(entropy, sb::append);
        String mnemonics = sb.toString();
        Logger.w("助记词:" + mnemonics);
        return Flowable.just(mnemonics);
}

2~5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public Flowable<HLWallet> generateWallet(Context context, String walletName, String password) {
    Flowable<String> flowable = mnemonics();

    return flowable
            .map(mnemonics -> {
                // 2. 由助记词得到种子
                byte[] seed = new SeedCalculator().calculateSeed(mnemonics, "");
                NetworkParameters params = MainNetParams.get();
                // 3. 生成根私钥 root private key
                DeterministicKey rootPrivateKey = HDKeyDerivation.createMasterPrivateKey(seed);
                // 4. 根私钥进行 priB58编码,得到测试网站上显示的数据
                String priv = rootPrivateKey.serializePrivB58(params);
                Logger.i("BIP32 Extended Private Key:" + priv);
                // 5. 生成 HD 钱包 , 由根私钥
                DeterministicHierarchy dh = new DeterministicHierarchy(rootPrivateKey);
                // 6. 定义父路径
                List<ChildNumber> parentPath = HDUtil.parsePath();
                // 7. 由父路径,派生出第一个子私钥 "new ChildNumber(0)" 表示第一个
                DeterministicKey child = dh.deriveChild(parentPath, true, true, new ChildNumber(0));
                Logger.w(" BIP32 Extended private Key:" + child.serializePrivB58(params));
                String childPrivateKey = child.getPrivateKeyAsHex();
                String childPublicKey = child.getPublicKeyAsHex();
                ECKeyPair childEcKeyPair = ECKeyPair.create(child.getPrivKeyBytes());
                String childAddress = Keys.getAddress(childEcKeyPair);
                String fullAddress = Constant.PREFIX_16 + childAddress;
                Logger.w("child privateKey:" + childPrivateKey + "\n" + "child publicKey:" + childPublicKey + "\n" + "address:" + fullAddress);
                WalletFile walletFile = org.web3j.crypto.Wallet.createLight(password, childEcKeyPair);
                String keystore = Singleton.get().gson.toJson(walletFile);
                Logger.w("keystore:\n" + keystore);
                WalletManager.shared().saveWallet(context, walletName, walletFile, mnemonics);
                return new HLWallet(mnemonics, walletName, childAddress);
            });
}

Note: 这里有个注意点,byte[] seed = new SeedCalculator().calculateSeed(mnemonics, "")这里的””是BIP39的密码.少数平台生成钱包使用用户输入的密码作为其密码,这将使得钱包不能通用.目前大多平台生成钱包时对其设置均为空字符串.

关于bitcoinj的使用比较不做赘述了.以上已经贴了主要的代码,下面我们主要讲解一下其他实现

novacrypto

这里使用novacrypto作者的BIP全家桶,我们重点讲解下这种实现

1
2
3
4
implementation 'io.github.novacrypto:BIP39:0.1.9'
implementation 'io.github.novacrypto:BIP44:0.0.3'
//implementation 'io.github.novacrypto:BIP32:0.0.9' // 有少许问题
implementation 'com.lhalcyon:bip32:1.0.0'   // 上面的BIP32有少许问题,笔者做了一些修改,已经上传lib

其中作者的BIP32库还有些许问题, 大家可以使用文末 demo 中的 BIP32 lib,笔者对其做了部分修改.

1 . 随机生成一组助记词,这里我们使用的是12个英文单词

1
2
3
4
5
6
7
8
public String generateMnemonics() {
        StringBuilder sb = new StringBuilder();
        byte[] entropy = new byte[Words.TWELVE.byteLength()];
        new SecureRandom().nextBytes(entropy);
        new MnemonicGenerator(English.INSTANCE)
                .createMnemonic(entropy, sb::append);
        return sb.toString();
}

2 . 使用助记词计算出Seed,得到master key ,根据BIP44派生地址,获取KeyPair

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public ECKeyPair generateKeyPair(String mnemonics){
    // 1. we just need eth wallet for now
    AddressIndex addressIndex = BIP44
            .m()
            .purpose44()
            .coinType(60)
            .account(0)
            .external()
            .address(0);
    // 2. calculate seed from mnemonics , then get master/root key ; Note that the bip39 passphrase we set "" for common
    ExtendedPrivateKey rootKey = ExtendedPrivateKey.fromSeed(new SeedCalculator().calculateSeed(mnemonics, ""), Bitcoin.MAIN_NET);
    Logger.i("mnemonics:" + mnemonics);
    String extendedBase58 = rootKey.extendedBase58();
    Logger.i("extendedBase58:" + extendedBase58);

    // 3. get child private key deriving from master/root key
    ExtendedPrivateKey childPrivateKey = rootKey.derive(addressIndex, AddressIndex.DERIVATION);
    String childExtendedBase58 = childPrivateKey.extendedBase58();
    Logger.i("childExtendedBase58:" + childExtendedBase58);

    // 4. get key pair
    byte[] privateKeyBytes = childPrivateKey.getKey();
    ECKeyPair keyPair = ECKeyPair.create(privateKeyBytes);

    // we 've gotten what we need
    String privateKey = childPrivateKey.getPrivateKey();
    String publicKey = childPrivateKey.neuter().getPublicKey();
    String address = Keys.getAddress(keyPair);

    Logger.i("privateKey:"+privateKey);
    Logger.i("publicKey:"+publicKey);
    Logger.i("address:"+ Constant.PREFIX_16+address);

    return keyPair;
}

这里可以清晰看出,我们已经可以得到私钥,公钥,地址.

当前的钱包app基本上都用所有币种公用一套助记词, 然后可以分别生成不同的钱包地址, 如果需要测试助记词, 和校验助记词生成的地址, 那么可以访问这个网站 : https://iancoleman.io/bip39/

我们需要校验的值主要为以上代码实例中打印日志的值.

3 . 通过keypair创建钱包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Flowable<HLWallet> generateWallet(Context context, 
                                         String password, 
                                         String mnemonics) {
    Flowable<String> flowable = Flowable.just(mnemonics);

    return flowable
            .map(s -> {
                ECKeyPair keyPair = generateKeyPair(s);
                WalletFile walletFile = Wallet.createLight(password, keyPair);
                HLWallet hlWallet = new HLWallet(walletFile);
                WalletManager.shared().saveWallet(context,hlWallet);
                return hlWallet;
            });
}

完整调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Flowable
        .just(password) // 用户输入的密码
        .filter(o -> validateInput(password,repassword)) // 校验密码
        .map(s -> InitWalletManager.shared().generateMnemonics())// 生成随机助记词
        .flatMap(s -> InitWalletManager.shared().generateWallet(mContext,password,s))//创建钱包
        .compose(ScheduleCompat.apply()) // 线程切换
        .subscribe(new HLSubscriber<HLWallet>(mContext,true) {
             @Override
             protected void success(HLWallet data) {  
             }

             @Override
             protected void failure(HLError error) {
             }
        });

以上即为以太坊创建钱包的主要内容.主要先理解概念,然后着手coding,事半功倍.

GitHub 系列教程代码已上传