Home 比特币-钱包备份和恢复
Post
Cancel

比特币-钱包备份和恢复

这篇学习下钱包备份和恢复相关的知识。

本人知识有限,如有错误和疏漏,请务必指正,多谢。

前言

之前 HD 钱包的文章介绍过钱包密钥的派生过程,主密钥(包括链码)是由熵、助记词、种子一步步推导出来的,而子密钥都是根据主密钥确定性派生出来的,所谓确定性就是说只要提供子密钥的路径,无论派生多少次,派生出来的密钥都是一样的。

那么其实对于用户来说,只要保留好熵、助记词、种子、主密钥(包括链码)中的任何一样都可以恢复出自己的钱包。但钱包一般只会提供助记词或主密钥给客户来备份,助记词要好过主密钥,有两点:

  • 助记词顾名思义,可以方便用户记忆,一般是 12 个单词,抄写到纸上之类的,也比较不容易出错。而主密钥(包括链码)就是 64 字节的不直观的数据而已。
  • 助记词可以推导出所有其他的要素,比如熵、种子、主密钥,但是主密钥无法推导出其他要素,因为主密钥是单向 hash 得到的,虽然不影响推导子密钥(也就不影响使用钱包),但是永远无法知道比如助记词是什么,不方便。

这里就有一个困惑很久的问题。

我们知道 HD 钱包是分层的,而且是可以无限派生的,我们每个人都有很多的密钥,虽然密钥是确定性的,但是如果只提供助记词或主密钥,钱包是如何知道,我们之前一共派生并使用过多少密钥呢?毕竟得知道这些,才能算出我们的资产。

另外一个问题是,钱包一般还提供了备份到文件的功能,这种之前说的有什么区别,到底备份了哪些东西

看完下面的介绍,相信大家跟我一样,对这两个问题,就会有比较清晰的解答。

正文

这里会粘贴一些代码,这些代码来自bitcoinj,是一个 java 实现的比特币协议代码。

备份钱包到文件到底备份些什么

其实备份钱包到文件,还是备份了很多东西的,这些东西能够加快恢复钱包的速度。

代码在后面了,主要的备份内容就是下面这些:

  • 网络ID 、钱包描述和版本
  • 所有本钱包的交易记录
  • key 相关内容,包括:种子、助记词、所有的key、对应的路径
  • 一些脚本,与公钥对应,主要用来从全节点获取交易信息用。
  • 最后一个块的 hash
  • scrypt 参数、Signer、Tags
  • key rotation time 信息,与比特币安全相关
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
    /**
     * Converts the given wallet to the object representation of the protocol buffers. This can be modified, or
     * additional data fields set, before serialization takes place.
     */
    public Protos.Wallet walletToProto(Wallet wallet) {
        Protos.Wallet.Builder walletBuilder = Protos.Wallet.newBuilder();
        walletBuilder.setNetworkIdentifier(wallet.getNetworkParameters().getId()); // 网络id
        if (wallet.getDescription() != null) {
            walletBuilder.setDescription(wallet.getDescription()); // 钱包描述
        }

        for (WalletTransaction wtx : wallet.getWalletTransactions()) {
            Protos.Transaction txProto = makeTxProto(wtx);
            walletBuilder.addTransaction(txProto); // 交易记录
        }

        walletBuilder.addAllKey(wallet.serializeKeyChainGroupToProtobuf()); // 所有key相关

        for (Script script : wallet.getWatchedScripts()) {
            Protos.Script protoScript =
                    Protos.Script.newBuilder()
                            .setProgram(ByteString.copyFrom(script.getProgram()))
                            .setCreationTimestamp(script.getCreationTimeSeconds() * 1000)
                            .build();

            walletBuilder.addWatchedScript(protoScript); // 脚本
        }

        // Populate the lastSeenBlockHash field.
        Sha256Hash lastSeenBlockHash = wallet.getLastBlockSeenHash(); // 最后一块区块信息
        if (lastSeenBlockHash != null) {
            walletBuilder.setLastSeenBlockHash(hashToByteString(lastSeenBlockHash));
            walletBuilder.setLastSeenBlockHeight(wallet.getLastBlockSeenHeight());
        }
        if (wallet.getLastBlockSeenTimeSecs() > 0)
            walletBuilder.setLastSeenBlockTimeSecs(wallet.getLastBlockSeenTimeSecs());

        // Populate the scrypt parameters.
        KeyCrypter keyCrypter = wallet.getKeyCrypter(); // scrypt 参数
        if (keyCrypter == null) {
            // The wallet is unencrypted.
            walletBuilder.setEncryptionType(EncryptionType.UNENCRYPTED);
        } else {
            // The wallet is encrypted.
            walletBuilder.setEncryptionType(keyCrypter.getUnderstoodEncryptionType());
            if (keyCrypter instanceof KeyCrypterScrypt) {
                KeyCrypterScrypt keyCrypterScrypt = (KeyCrypterScrypt) keyCrypter;
                walletBuilder.setEncryptionParameters(keyCrypterScrypt.getScryptParameters());
            } else {
                // Some other form of encryption has been specified that we do not know how to persist.
                throw new RuntimeException("The wallet has encryption of type '" + keyCrypter.getUnderstoodEncryptionType() + "' but this WalletProtobufSerializer does not know how to persist this.");
            }
        }

        if (wallet.getKeyRotationTime() != null) {
            long timeSecs = wallet.getKeyRotationTime().getTime() / 1000;
            walletBuilder.setKeyRotationTime(timeSecs); // rotation time
        }

        populateExtensions(wallet, walletBuilder);

        for (Map.Entry<String, ByteString> entry : wallet.getTags().entrySet()) {
            Protos.Tag.Builder tag = Protos.Tag.newBuilder().setTag(entry.getKey()).setData(entry.getValue());
            walletBuilder.addTags(tag); // Tags
        }

        for (TransactionSigner signer : wallet.getTransactionSigners()) {
            // do not serialize LocalTransactionSigner as it's being added implicitly
            if (signer instanceof LocalTransactionSigner)
                continue;
            Protos.TransactionSigner.Builder protoSigner = Protos.TransactionSigner.newBuilder();
            protoSigner.setClassName(signer.getClass().getName());
            protoSigner.setData(ByteString.copyFrom(signer.serialize()));
            walletBuilder.addTransactionSigners(protoSigner); // Signer
        }

        // Populate the wallet version.
        walletBuilder.setVersion(wallet.getVersion()); // 钱包版本

        return walletBuilder.build();
    }

所以,就是基本备份了钱包的所有用户数据。。钱包交易越多,密钥越多,备份后的钱包就越大。

一般备份钱包到文件的时候,需要用户输入一个密码,钱包数据是用这个密码加密过的,恢复钱包的时候,也需要提供这个密码。备份后的钱包文件,最好离线保存起来。

这种钱包很好恢复,因为包含了所有的数据,包括所有的密钥和密钥对应的路径等,只需要原样载入数据库就可以。那么在比如只提供助记词的时候,钱包是如何恢复的?

钱包如何知道密钥数量

只知道助记词的情况下,钱包会先计算出来种子,然后根据种子来生成根密钥, 并按照索引递增的方式,推导出一定数量的子密钥,这个数量一般是预设的,比如 30 个子密钥。

当然这些子密钥可能不足或者超过了用户所有使用过的密钥数量,这都没关系。

然后钱包将这些密钥对应的地址发送给全节点来获取所有相关的交易数据,因为交易数据的输出脚本部分都会包含地址,所以可以通过对比地址来知道有哪些交易是与这些地址相关的。

为什么钱包不自己搜索所有的交易数据?因为钱包是轻节点,只保留了区块链的一部分,没有所有的块数据,只能依靠全节点来检索数据。

全节点是什么?保有一份完整的、最新的区块链拷贝的节点都叫做全节点。全节点能够独立自主地校验所有交易,而不需借由任何外部参照。常见的有 Reference Client、Full Block Chain Node、Solo Miner。

涉及到用户隐私保护,钱包发送给全节点的地址信息,是通过 bloom 处理过的,是不完整的信息,全节点会发回所有包含这些信息的交易数据,然后钱包根据完整的地址来筛选信息,得到自己相关的信息。

然后全节点将这些信息发送回钱包,钱包就知道了这些地址中哪些是使用过的,因此就知道了两个信息,①对应的哪些密钥是使用过的,② 各个地址中有多少余额。

如果预生成的密钥,都已经使用过,那么就说明所有的密钥可能不止这些,那么钱包会继续生成一批密钥,然后再将这些密钥对应的地址发送给全节点来获取信息,直到预生成的密钥,没有全部使用为止。

通过这个过程,钱包就知道了用户所有的密钥和余额信息。

参考:

bitcoinj 代码实现

This post is licensed under CC BY 4.0 by the author.
Contents