├── .gitignore ├── LICENSE ├── README.md ├── SUMMARY.md ├── build.md ├── introduction.md ├── keystruct.md ├── keystruct ├── addr.md ├── cblock.md ├── cblockindex.md ├── cnode.md ├── cnodestate.md ├── global.md ├── key.md ├── keystore.md ├── params.md ├── signal.md └── wallet.md ├── net ├── connode.md ├── dnsseed.md ├── message1.md ├── message2.md ├── message3.md └── socket.md ├── start ├── setup01.md ├── setup02.md ├── setup03.md ├── setup04.md ├── setup05.md ├── setup06.md ├── setup07.md ├── setup08.md ├── setup09.md ├── setup10.md ├── setup11.md ├── setup12.md ├── setup13.md └── start.md └── wallet ├── GenerateNewSeed.md ├── LoadWallet.md ├── TopUpKeyPool.md ├── createwallet.md ├── getnewaddress.md ├── method.md └── walletsummary.md /.gitignore: -------------------------------------------------------------------------------- 1 | _book 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 比特币源码分析 2 | ======= 3 | 4 | 《比特币源码分析》是《精通比特币》的非官方代码版本, 谨以此献给热爱比特币的程序员们! 5 | 6 | ### 快速阅读传送门 7 | - Github快速阅读任一章节:[进入目录](https://github.com/joepeak/bitcoindecode/blob/master/SUMMARY.md) 8 | - Gitbook完整顺序地阅读:[进入Gitbook](https://joepeak.gitbook.io/btcdecode/) 9 | 10 | 11 | # 前言 12 | 13 | 2008年比特币诞生,原本只是一个密码学极客之间的玩物,没想到犹如打开的潘多拉盒子,慢慢席卷全球。在08 年以前还没有人能成功地研发出一个运行良好的数字货币出来,直到比特币问世;另外,区块链作为比特币的底层 技术,在此之前也是闻所未闻。那么区块链到底有什么魔力,让整个世界为之疯狂呢? 14 | 15 | ## 作者 16 | 区小白 @joepeak 17 | 18 | 颖君 @Irischen666 19 | 20 | 21 | ## 欢迎建议指正或直接贡献代码 22 | https://github.com/joepeak/bitcoindecode/issues 23 | 24 | ### 微信打赏支持: 25 | > 26 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # 比特币源码情景分析 2 | 3 | * [简介](README.md) 4 | * [序](introduction.md) 5 | 6 | * [从零编译比特币](build.md) 7 | 8 | * 系统启动流程 9 | * [系统启动过程之鸟瞰](start/start.md) 10 | * [第一步:应用基本设置](start/setup01.md) 11 | * [第二步:初始参数设置](start/setup02.md) 12 | * [第三步:参数内部转换](start/setup03.md) 13 | * [第四步:应用程序初始化](start/setup04.md) 14 | * [第五步:钱包完整性验证](start/setup05.md) 15 | * [第六步:网络初始化](start/setup06.md) 16 | * [第七步:加载区块链](start/setup07.md) 17 | * [第八步:开始建立索引](start/setup08.md) 18 | * [第九步:加载钱包](start/setup09.md) 19 | * [第十步:数据目录维护](start/setup10.md) 20 | * [第十一步:导入区块](start/setup11.md) 21 | * [第十二步:启动节点](start/setup12.md) 22 | * [第十三步:启动结束](start/setup13.md) 23 | 24 | * P2P 网络建立流程 25 | * [套接字的处理](net/socket.md) 26 | * [DNS 种子节点的处理](net/dnsseed.md) 27 | * [连接到对等节点的处理](net/connode.md) 28 | * [消息处理上篇-消息线程分析](net/message1.md) 29 | * [消息处理中篇-协义分析一](net/message2.md) 30 | * [消息处理下篇-协义分析二](net/message3.md) 31 | 32 | * 密钥、地址和钱包 33 | * [创建钱包](wallet/createwallet.md) 34 | * [创建公钥](wallet/GenerateNewSeed.md) 35 | * [填充密钥池](wallet/TopUpKeyPool.md) 36 | * [加载钱包](wallet/LoadWallet.md) 37 | * [钱包总结](wallet/walletsummary.md) 38 | * [生成地址](wallet/getnewaddress.md) 39 | 40 | * 交易 41 | * [交易转账](transaction/sendtoaddress) 42 | 43 | * 比特币常用数据结构 44 | * [网络地址相关](keystruct/addr.md) 45 | * [对等节点结构](keystruct/cnode.md) 46 | * [对等节点状态](keystruct/cnodestate.md) 47 | * [区块索引对象](keystruct/cblockindex.md) 48 | * [区块对象](keystruct/cblock.md) 49 | 50 | -------------------------------------------------------------------------------- /build.md: -------------------------------------------------------------------------------- 1 | # 从源代码编译比特币 2 | 3 | ##写在开始之前,为什么你一定要学习区块链技术? 4 | 5 | 技术的变革和迭代一直在飞速发展中,作为有着15年程序开发经验的我,常常在思考现在的我们到底改如何做,到底应该学习些什么,才能跟上新的时代变革,保持自身的竞争力,并且能为这个世界带来更好的改变呢? 6 | 7 | 答案是,学习新技术,成为紧跟时代发展趋势的稀缺技术人才。而毫无疑问,比特币区块链技术是绝对不容错过的。 8 | 9 | 当我研究了比特币区块链之后,更加确信了这一点。比特币区块链技术解决了人和人之间的信任问题,是对生产力和生产关系的一次变革,而这必将影响人类社会的发展。 10 | 11 | 想到就要做到,于是我开始深入研究了比特币区块链技术,从0开始一行行的代码跑起来,遇到过很多坑,花了很多时间和精力爬坑。现在我把这些凝聚时间和心血的学习资料整理成文档写成教程,希望能够帮助你在学习的过程中少些弯路。 12 | 13 | 14 | 15 | ##准备工作 16 | 17 | 没有亲自跑一遍代码,不算真正的学习。 18 | 19 | 今天我们开始从零编译比特币源代码。 20 | 21 | ####下载比特币源代码 22 | 23 | 首先要做的就是从[github](https://github.com/bitcoin/bitcoin)上下载比特币的源代码,其中 `doc` 目录为比特币文档,`src` 为系统源代码,`test` 为测试代码的目录。具体怎么下载,想必大家都用过 `git` 和 `github` ,就不用我细说了。 24 | 25 | 当我们下载完源代码之后,进入 `doc` 子目录,找到 build-xxx.md 文档,xxx 代表了不同的系统,当前支持的系统有 freebas、netbsd、openbsd、osx、unix、windows 等,根据你的系统参考不同的安装文档。比如,我的系统为 Mac,对应的就是 build-osx.md,打开这个文档会看到构建说明和一些备注。 26 | 27 | ####命令行工具准备 28 | 29 | 在 Mac 系统下,必备的工具就是 `xcode` 命令行工具,我们通过输入如下命令进行安装: 30 | 31 | xcode-select --install 32 | 33 | 当弹出窗口出现时,选择 `安装`。 34 | 35 | ####安装依赖 36 | 37 | 当命令行工具安装之后,接下来我们要做的就是安装依赖,在些特别推荐使用[Homebrew](https://brew.sh/),这是 Mac 下面安装应用的必备神器。 38 | 39 | 当 Homebrew 安装完成之后,就开始安装编译比特币的各种依赖了,命令如下: 40 | 41 | brew install automake berkeley-db4 libtool boost miniupnpc openssl pkg-config protobuf python qt libevent qrencode 42 | 43 | 如果你需要生成 `dmg` 可执行文件,那么还需要 RSVG,安装命令如下: 44 | 45 | brew install librsvg 46 | 47 | 如果需要 ZMQ,那么还需要相关的库,安装命令如下: 48 | 49 | brew install zmq 50 | 51 | ## 具体步骤 52 | 53 | 当依赖安装完成之后,就真正开始编译比特币。 54 | 55 | 1. **首先,进入比特币根目录。命令如下:** 56 | 57 | cd bitcoin 58 | 59 | 2. **然后,开始编译比特币源代码。命令如下:** 60 | 61 | ./autogen.sh 62 | ./configure --enable-debug 63 | make clean && make -j4 64 | 65 | 如果在编译过程中,出现如下错误: 66 | 67 | ld: symbol(s) not found for architecture x86_64 68 | clang: error: linker command failed with exit code 1 (use -v to see invocation) 69 | make[2]: *** [bitcoind] Error 1 70 | make[1]: *** [all-recursive] Error 1 71 | make: *** [all-recursive] Error 1 72 | 73 | 那么需要明确指定 Boost 位置: 74 | 75 | ./configure --enable-debug --with-boost=/usr/local/opt/boost 76 | 77 | 如果你不需要图形界面,那么在执行 `./configure` 时需要加入 `--without-gui` 标志,即 `./configure --without-gui`。另外,在 Mac 系统下,为了调试比特币代码,需要把 `configure` 文件中的所有 `-g -O2` 替换为 `-g`,这是因为 Mac 下的 LLDB 存在 bug,导致某些变量不可用。 78 | 79 | 当你看到下面的图片时,恭喜你编译成功了。 80 | 81 | ![编译成功](http://ocie6rxms.bkt.clouddn.com/build-bitcoind2.png) 82 | 83 | 比特币编译成功时,会在 src 目录下面生成4个可执行的命令:bitcoind、bitcoin-cli、bitcoin-tx、qt/bitcoin-qt,如红框所示。 84 | 85 | 3. **强烈建议,你执行下面的命令来运行一遍单元测试:** 86 | 87 | make check 88 | 89 | 通常这一步是不会出错的。 90 | 91 | 4. **可选地,你也可以生成一个 dmg,命令如下:** 92 | 93 | make deploy 94 | 95 | 执行这个命令后,系统会提示你把应用放在 Application 下面。最终应用安装在 `/Applications/Bitcoin-Qt.app` 下。 96 | 97 | 当比特币编译完成后,万事大吉,只欠运行了。 98 | 99 | 5. **设置下 RPC用户及密码** 100 | 101 | 但是在运行比特币核心客户端之前,强烈建议你设置下 RPC用户及密码,这样你才可使用系统提供的所有 RPC 命令。 102 | 103 | 具体命令如下: 104 | 105 | echo -e "rpcuser=bitcoinrpc\nrpcpassword=$(xxd -l 16 -p /dev/urandom)" > "/Users/${USER}/Library/Application Support/Bitcoin/bitcoin.conf" 106 | 107 | chmod 600 "/Users/${USER}/Library/Application Support/Bitcoin/bitcoin.conf" 108 | 109 | 执行完上面两个命令之后,我们来确认是否设置成功。 110 | 111 | 首先执行: 112 | 113 | `ls -l "/Users/${USER}/Library/Application Support/Bitcoin/bitcoin.conf"` 114 | 115 | 来确认文件的模式为 `-rw-r--r—`,如图下图: 116 | 117 | ![img](http://ocie6rxms.bkt.clouddn.com/bitcoin-rpc.png?nsukey=LX9ET1TIhh5ZtGUW7XMdJJYmz%2Ff%2FcrS6Squ3%2F5Pl%2Fb74yAP%2FvG1Z29Vn471wb4tbr1FCEIRmvofan7J0ON%2FCo5yQBnVmRxDY7BeTbX8Srd0TmARzFDEsFDUItKSMwy9uGzf%2BG6L0lusFk%2FgGO0osGjGq1e4iIZUYzqPOWq4r0I%2ByfGsA%2FNdUOe7Sh99aynV%2BdAhHru23S8bYCtdf1XM9QA%3D%3D) 118 | 119 | 120 | 121 | 然后再执行`vi "/Users/${USER}/Library/Application Support/Bitcoin/bitcoin.conf"` 122 | 123 | 看到文件内容如下即为设置成功。 124 | 125 | ![img](http://ocie6rxms.bkt.clouddn.com/bitcoind-conf.png) 126 | 127 | 128 | 129 | 当设置完 RPC 用户及密码之后,下面就开始输入最最重要的命令: 130 | 131 | ./src/bitcoind -testnet # -testnet 代表的是测试网络,如果不加这个标志,那么就连接到比特币主网络。作为演示,此处连接到比特币测试网络。 132 | 133 | 键入上面的命令并按下回车键。 134 | 135 | ![比特币运行图](http://ocie6rxms.bkt.clouddn.com/bitcoind-running.png) 136 | 137 | 恭喜你,你的比特币之路已经开始。 138 | 139 | 140 | 141 | 我是区小白,区块链开发者,区块链技术爱好者,深入研究比特币,以太坊,EOS Dash,Rsk,Java, Nodejs,PHP,Python,C++ 现为Ulord全球社区联盟(优得社区)核心开发者。 142 | 143 | 我希望能聚集更多区块链开发者,一起学习共同进步。 144 | 145 | 敬请期待下一篇文章:如何启动比特币系统并加入比特币网络 146 | 147 | 148 | 149 | 学习过程中如有遇到任何问题,以及想要学习更多内容请加 150 | 151 | QQ群:253968045 152 | 153 | QQ号:77078193 或者 705706498 154 | 155 | 微信:joepeak 156 | 157 | ![image-20180829140051383](http://ocie6rxms.bkt.clouddn.com/quxiaobai.jpeg) 158 | 159 | 原文发布于: 160 | 161 | http://www.uldfans.com/comment.html?reviewId=cacd3113-5222-4cd6-8014-029bce4e22ce 162 | 163 | -------------------------------------------------------------------------------- /introduction.md: -------------------------------------------------------------------------------- 1 | # 序 2 | 3 | 这是一本关于比特币源代码的分析文章, 谨以此献给热爱比特币的程序员们! 4 | 5 | 比特币网络始于2009年,基于中本聪发布的参考实施指南,之后由许多其他程序员进行修订。为比特币提供安全 性和弹性的工作量证明算法(挖掘)的实施以指数级增长,现在超过了世界顶级超级计算机的组合处理能力。中本聪于2011年4月退出公众视线,将代码和网络的责任放在一个蓬勃发展的志愿者小组身上。中本聪和任何人都没有对比特币系统进行个人控制,这个系统基于完全透明的数学原理,开放源代码和参与者之间的共识持续运行。 6 | 7 | 作为一个程序员,你是不是对这一切感到好奇呢,从本章开始,我会通过解读源代码来带领你一起领略这一系统的魅力。下面简要介绍本系列教程的大纲: 8 | 9 | 1. 系统启动 10 | 11 | 本部分主要讲述比特币系统的启动过程。包括命令参数的解析、RPC 命令的注册、钱包的验证、网络初始化、区块链数据的加载与对等节点的启动等。 12 | 13 | 2. 网络处理 14 | 15 | 本部分讲述比特币底层 P2P 网络建立的过程。包括:套接字的处理、DNS种子节点的查找、如何连接到别的对等节点、消息处理的框架等。 16 | 17 | 3. 消息处理 18 | 19 | 本部分讲述比特币协义的处理,任何想要与比特币通信的应用都要满足这些协义。包括了:对等节点的握手、保持节点连接的处理、获取地址的处理、设置费率与紧凑格式的请求、获取区块的处理等。 20 | 21 | 4. 交易处理 22 | 23 | 本部分讲述的地址、钱包、交易、挖矿和脚本等相关内容。 24 | 25 | 5. 基于比特币构建应用程序 26 | 27 | 本部分会仔细系统提供的 RPC 接口,以及对这些接口进行梳理,探讨可以构建一些应用。 28 | 29 | 30 | -------------------------------------------------------------------------------- /keystruct.md: -------------------------------------------------------------------------------- 1 | # 比特币关键数据结构 2 | 3 | 4 | ## 1、共识参数 5 | 6 | ## 2、节点状态 7 | 8 | 节点状态由 `CNodeState` 结构体保存,分别由以下信息: 9 | 10 | - fCurrentlyConnected 11 | 12 | 是否已经完全建立连接。 13 | 14 | - nMisbehavior 15 | 16 | 不良节点的积分 17 | 18 | - fShouldBan 19 | 20 | 节点是否应该被禁止,除非是白名单 21 | 22 | - rejects 23 | 24 | - pindexBestKnownBlock 25 | 26 | - hashLastUnknownBlock 27 | 28 | 对等节点发布的最后一个未知区块的哈希值。 29 | 30 | - pindexLastCommonBlock 31 | 32 | - pindexBestHeaderSent 33 | 34 | - nUnconnectingHeaders 35 | 36 | - fSyncStarted 37 | 38 | - nStallingSince 39 | 40 | 停止下载区块的时间。 41 | 42 | - vBlocksInFlight 43 | 44 | - nDownloadingSince 45 | 46 | - fPreferHeaders 47 | 48 | 布尔值,对于区块公告,对等接点愿意接收 invs 还是 headers 消息。 49 | 50 | - fPreferHeaderAndIDs 51 | 52 | 布尔值,对于区块公告,对等接点愿意接收 invs 还是 cmpctblocks 消息。 53 | 54 | 55 | ## 3、对等节点信息 56 | 57 | 对等节点信息用 `CNode` 类来表示,重要的字段有下面一些。 58 | 59 | - vBlockHashesToAnnounce 60 | 61 | 使用区块头部进行区块公告的集合。 62 | 63 | -------------------------------------------------------------------------------- /keystruct/addr.md: -------------------------------------------------------------------------------- 1 | # 比特币关键数据结构之地址 2 | 3 | ## 1、CNetAddr 类 4 | 5 | 这个类的定义在 `netaddress.h` 文件中,表示节点的 IPV4 或 IPV6 地址。 6 | 7 | 主要属性如下: 8 | 9 | - ip 10 | 11 | IP地址,无符号16位的字符串。 12 | 13 | - scopeId 14 | 15 | 16 | ## 2、CService 17 | 18 | 这个类的定义同样在 `netaddress.h` 文件中,组合了 IP 地址和 TCP 的端口号,继承于 `CNetAddr` 类。 19 | 20 | 主要属性如下: 21 | 22 | - port 23 | 24 | 主机的端口 25 | 26 | ## 3、CAddress 27 | 28 | 这个类的定义在 `protocol.h` 文件中,表示对等节点信息,继承于 `CService` 类。 29 | 30 | 主要属性如下: 31 | 32 | - nServices 33 | 34 | 节点所支持的服务。 35 | 36 | - nTime 37 | 38 | 39 | ## 4、CAddrInfo 40 | 41 | 这个类的定义在 `addrman.h` 文件中,继承于 `CAddress`,增加了一些统计信息。 42 | 43 | 主要属性如下: 44 | 45 | - nLastTry 46 | 47 | - nLastCountAttempt 48 | 49 | - source 50 | 51 | - nLastSuccess 52 | 53 | - nAttempts 54 | 55 | - nRefCount 56 | 57 | - fInTried 58 | 59 | - nRandomPos 60 | 61 | 62 | ## 5、CAddrMan 63 | 64 | 这个类的定义在 `addrman.h` 文件中,表示随机的地址管理器。 65 | 66 | 主要属性如下: 67 | 68 | - nIdCount 69 | 70 | 最后一个使用的 nId,也是下面属性的 Key。 71 | 72 | - mapInfo 73 | 74 | 地址信息的映射,Key 是 nIdCount,Value 为 `CAddrInfo` 对象。在获得地址成功后,保存在本属性中。 75 | 76 | 77 | -------------------------------------------------------------------------------- /keystruct/cblock.md: -------------------------------------------------------------------------------- 1 | # 比特币关键数据结构之区块相关 2 | 3 | ## 区块对象 4 | 5 | 区块对象用 `CBlock` 类来表示,继承自 `CBlockHeader` ,重要字段如下: 6 | 7 | - vtx 8 | 9 | 交易对象的向量集。 10 | 11 | - fChecked 12 | 13 | 是否已经检查。只保存在内存中。 14 | 15 | ## 区块头部 16 | 17 | 区块头部对象用 `CBlockHeader` 类表示,重要字段如下: 18 | 19 | - nVersion 20 | 21 | 区块的版本号。 22 | 23 | - hashPrevBlock 24 | 25 | 前一个区块的哈希。 26 | 27 | - hashMerkleRoot 28 | 29 | 区块的默克尔数。 30 | 31 | - nTime 32 | 33 | 区块生成时间。 34 | 35 | - nBits 36 | 37 | 区块的难度数。 38 | 39 | - nNonce 40 | 41 | 区块的随机数。 42 | 43 | -------------------------------------------------------------------------------- /keystruct/cblockindex.md: -------------------------------------------------------------------------------- 1 | # 比特币关键数据结构之区块索引对象 2 | 3 | 区块索引对象用 `CBlockIndex` 类来表示,重要字段有下面一些。 4 | 5 | - phashBlock 6 | 7 | 指向区块的哈希 8 | 9 | - pprev 10 | 11 | 指向前一个区块的索引指针,类型为 ` CBlockIndex*`。 12 | 13 | - pskip 14 | 15 | 指向更前一个区块的索引指针,类型为 ` CBlockIndex*`。 16 | 17 | - nHeight 18 | 19 | 区块在区块链中的高度,创世区块调试为0。 20 | 21 | - nFile 22 | 23 | 区块存在哪一个 `blk?????.dat` 文件中。 24 | 25 | - nDataPos 26 | 27 | 区块在 `blk?????.dat` 文件中的偏移量。 28 | 29 | - nUndoPos 30 | 31 | 区块的 undo data 在 `rev?????.dat` 文件中的偏移量。 32 | 33 | - nChainWork 34 | 35 | 包含当前区块在内的总的工作量,只存在于内存中。 36 | 37 | - nTx 38 | 39 | 区块的交易数量,只存在于内存中。 40 | 41 | - nChainTx 42 | 43 | 包含当前区块在内的总的交易数量,只存在于内存中。 44 | 45 | - nStatus 46 | 47 | 区块的验证状态。可能的值为: 48 | 49 | - BLOCK_VALID_HEADER 50 | 51 | 版本 OK,哈希满足声称的 PoW,交易数大于等于1 小于等于最大数量,时间戳不在未来。 52 | 53 | - BLOCK_VALID_TREE 54 | 55 | 所有的父区块头部已经找到,难度也匹配,时间戳大于等于 median previous。隐含着所有父亲都在树上。 56 | 57 | - BLOCK_VALID_TRANSACTIONS 58 | 59 | 只有第一个交易是 coinbase,coinbase 输入脚本的长度大于等于 2 小于等于 100,所有交易是 OK 的,没有重复的交易 ID,sigops, size, merkle root。 60 | 61 | - BLOCK_VALID_CHAIN 62 | 63 | 输出没有超过输入,没有双重花费,coinbase 输出是 OK 的,没有使用时间过短的 coinbase,BIP30。 64 | 65 | - BLOCK_VALID_SCRIPTS 66 | 67 | 脚本和签名是 Ok 的。 68 | 69 | - BLOCK_VALID_MASK 70 | 71 | 所有有效位,等于 `BLOCK_VALID_HEADER | BLOCK_VALID_TREE | BLOCK_VALID_TRANSACTIONS | BLOCK_VALID_CHAIN | BLOCK_VALID_SCRIPTS` 72 | 73 | - BLOCK_HAVE_DATA 74 | 75 | 所有区块在 `blk*.dat` 文件中可用。 76 | 77 | - BLOCK_HAVE_UNDO 78 | 79 | undo data available in rev*.dat 80 | 81 | - BLOCK_HAVE_MASK 82 | 83 | 等于 `BLOCK_HAVE_DATA | BLOCK_HAVE_UNDO` 84 | 85 | - BLOCK_FAILED_VALID 86 | 87 | 最后达到有效性后的阶段失败 88 | 89 | - BLOCK_FAILED_CHILD 90 | 91 | descends from failed block 92 | 93 | - BLOCK_FAILED_MASK 94 | 95 | 等于 `BLOCK_FAILED_VALID | BLOCK_FAILED_CHILD` 96 | 97 | - BLOCK_OPT_WITNESS 98 | 99 | block data in blk*.data was received with a witness-enforcing client 100 | 101 | - nVersion 102 | 103 | 区块头部的版本号。 104 | 105 | - hashMerkleRoot 106 | 107 | 区块头部的默克尔根。 108 | 109 | - nTime 110 | 111 | 区块头部的时间戳。 112 | 113 | - nBits 114 | 115 | 区块头部的难度数。 116 | 117 | - nNonce 118 | 119 | 区块头部的随机数。 120 | 121 | - nSequenceId 122 | 123 | - nTimeMax 124 | 125 | 126 | -------------------------------------------------------------------------------- /keystruct/cnode.md: -------------------------------------------------------------------------------- 1 | # 比特币关键数据结构之对等节点信息 2 | 3 | 对等节点信息用 `CNode` 类来表示,重要的字段有下面一些。 4 | 5 | - nServices 6 | 7 | 节点支持的服务,接收 `version` 消息时,设置的。当前支持的服务有: 8 | 9 | - NODE_NETWORK 10 | 11 | 节点可以提供完整的区块而不仅仅是区块头。所有没有进行修剪的比特币核心客户端都是这个值,SPV客户端和其他轻量级客户端不设置这个值。 12 | 13 | - NODE_GETUTXO 14 | 15 | 节点支持 `getutxo` 协议请求,比特币核心客户端不支持这个,但 Bitcoin XT 补丁支持这个。详见 BIP 0064。 16 | 17 | - NODE_BLOOM 18 | 19 | 节点有能力并且愿意支持布隆过滤器,比特币核心默认支持此功能而不用公告这个位。详见 BIP 0111。 20 | 21 | - NODE_WITNESS 22 | 23 | 节点可以响应包含隔离见证的区块和交易请求。详见 BIP 0144。 24 | 25 | - NODE_XTHIN 26 | 27 | 节点支持 Xtreme Thinblocks。 28 | 29 | - NODE_NETWORK_LIMITED 30 | 31 | 基本等于 `NODE_NETWORK`,除了只提供 288 个区块,即2天内的区块。 32 | 33 | - hSocket 34 | 35 | - nSendSize 36 | 37 | - nSendOffset 38 | 39 | - nSendBytes 40 | 41 | - vSendMsg 42 | 43 | - vProcessMsg 44 | 45 | - nProcessQueueSize 46 | 47 | - vRecvGetData 48 | 49 | 接收到的请求数据队列,接收 `getdata` 消息时,设置的。 50 | - nRecvBytes 51 | 52 | - nRecvVersion 53 | 54 | 版本信息,服务器发送的版本与 70015 这个版本的小者,接收 `verack` 消息时,设置的。 55 | 56 | - nLastSend 57 | 58 | - nLastRecv 59 | 60 | - nTimeConnected 61 | 62 | - nTimeOffset 63 | 64 | - addr 65 | 66 | 对待节点的地址对象,类型为 `CAddress`。 67 | 68 | - addrBind 69 | 70 | - nVersion 71 | 72 | 客户端发送的版本,接收 `version` 消息时,设置的。 73 | 74 | - fWhitelisted 75 | 76 | 布尔型,节点是否为白名单节点 77 | 78 | - fFeeler 79 | 80 | 布尔型,节点是否为临时的引导节点 81 | 82 | - fOneShot 83 | 84 | - m_manual_connection 85 | 86 | 布尔型,节点是否为手工指定连接的 87 | 88 | - fClient 89 | 90 | 布尔型,节点是否可以提供区块服务。如果节点没有设置 `NODE_NETWORK`、或 `NODE_NETWORK_LIMITED` ,这个值为假,即不能提供区块服务。如果设置其中一个,这个值就为真,即可以提供区块服务。 91 | 92 | - m_limited_node 93 | 94 | 布尔型,节点是否为限制型节点。节点设置了 `NODE_NETWORK_LIMITED`,但没有设置 `NODE_NETWORK`,这值就为真。 95 | 96 | - fInbound 97 | 98 | 布尔型,节点是否为入站节点 99 | 100 | - fSuccessfullyConnected 101 | 102 | 布尔型,对等节点完全连接成功。只有在收到版本确认时,才会设置这个标志。 103 | 104 | - fDisconnect 105 | 106 | 布尔型,节点是否已经断开 107 | 108 | - fRelayTxes 109 | 110 | 布尔型,是否中继交易,接收 `version` 消息时,设置的。 111 | 112 | - fSentAddr 113 | 114 | 是否发送过请求地址标志。 115 | 116 | - grantOutbound 117 | 118 | - pfilter 119 | 120 | 类型为 CBloomFilter 的智能指针,在接收 `filterload` 消息时设置。 121 | 122 | - nRefCount 123 | 124 | - nKeyedNetGroup 125 | 126 | - fPauseRecv 127 | 128 | - fPauseSend 129 | 130 | - mapSendBytesPerMsgCmd 131 | 132 | - mapRecvBytesPerMsgCmd 133 | 134 | - hashContinue 135 | 136 | - nStartingHeight 137 | 138 | 节点最后一个区块的高度,也即节点开始接收区块的高度,接收 `version` 消息时,设置的。 139 | 140 | - vAddrToSend 141 | 142 | 等待发送的地址,自身要发送的地址也放在这个集合中。 143 | 144 | - addrKnown 145 | 146 | 已知的地址集合,类型为 `CRollingBloomFilter` 。 147 | 148 | - fGetAddr 149 | 150 | 布尔型,是否向这个节点请求过地址。 151 | 152 | - setKnown 153 | 154 | - nNextAddrSend 155 | 156 | - nNextLocalAddrSend 157 | 158 | - filterInventoryKnown 159 | 160 | - setInventoryTxToSend 161 | 162 | - vInventoryBlockToSend 163 | 164 | - setAskFor 165 | 166 | - mapAskFor 167 | 168 | - nNextInvSend 169 | 170 | - vBlockHashesToAnnounce 171 | 172 | 使用区块头部进行区块公告的集合 173 | 174 | - fSendMempool 175 | 176 | - timeLastMempoolReq 177 | 178 | - nLastBlockTime 179 | 180 | - nLastTXTime 181 | 182 | - nPingNonceSent 183 | 184 | 无符号 64 位,期待的 pong 中继,如果是 0,就是非期待的。 185 | 186 | - nPingUsecStart 187 | 188 | - nPingUsecTime 189 | 190 | - nMinPingUsecTime 191 | 192 | - fPingQueued 193 | 194 | - minFeeFilter 195 | 196 | - lastSentFeeFilter 197 | 198 | - nextSendTimeFeeFilter 199 | 200 | - nLocalHostNonce 201 | 202 | 在进行 `version` 时,生成的随机值。 203 | 204 | - nLocalServices 205 | 206 | - nMyStartingHeight 207 | 208 | - nSendVersion 209 | 210 | 版本信息,客户端发送的版本与 70015 这个版本的小者,接收 `version` 消息时,设置的。 211 | 212 | - vRecvMsg 213 | 214 | - addrName 215 | 216 | - addrLocal 217 | 218 | 对等节点的地址,接收 `version` 消息时,设置的。 219 | 220 | - hashContinue 221 | 222 | - nLastTXTime 223 | 224 | 最后一次收到交易的时间 225 | 226 | - nLastBlockTime 227 | 228 | 最后一次收到区块的时间 229 | 230 | -------------------------------------------------------------------------------- /keystruct/cnodestate.md: -------------------------------------------------------------------------------- 1 | # 比特币关键数据结构节点状态 2 | 3 | 节点状态由 `CNodeState` 结构体保存,分别由以下信息: 4 | 5 | - fCurrentlyConnected 6 | 7 | 是否已经完全建立连接。接收 `verack` 消息时,设置的。 8 | 9 | - nMisbehavior 10 | 11 | 节点的不良积分 12 | 13 | - fShouldBan 14 | 15 | 节点是否应该被禁止,除非是白名单 16 | 17 | - rejects 18 | 19 | - pindexBestKnownBlock 20 | 21 | - hashLastUnknownBlock 22 | 23 | 对等节点发布的最后一个未知区块的哈希值。 24 | 25 | - pindexLastCommonBlock 26 | 27 | - pindexBestHeaderSent 28 | 29 | 发送给对等节点的最佳区块索引。 30 | 31 | - nUnconnectingHeaders 32 | 33 | 未连接到区块链上的头部数量。 34 | 35 | - fSyncStarted 36 | 37 | - nStallingSince 38 | 39 | 停止下载区块的时间。 40 | 41 | - vBlocksInFlight 42 | 43 | - nDownloadingSince 44 | 45 | - fPreferHeaders 46 | 47 | 布尔值,对于区块公告,对等接点愿意接收 invs 还是 headers 消息。 48 | 49 | - fPreferHeaderAndIDs 50 | 51 | 布尔值,对于区块公告,对等接点愿意接收 invs 还是 cmpctblocks 消息。 52 | 53 | - fHaveWitness 54 | 55 | 布尔型,节点是否支持隔离见证。接收版本消息时设置的。 56 | 57 | - fPreferredDownload 58 | 59 | 节点是否为首选的下载节点。 60 | 61 | - fProvidesHeaderAndIDs 62 | 63 | 布尔型,当我们请求时,节点是否可以返回 cmpctblocks 给我们。 64 | -------------------------------------------------------------------------------- /keystruct/global.md: -------------------------------------------------------------------------------- 1 | # 全局变量 2 | 3 | ## validation.h 文件中的全局变量 4 | 5 | 1. pcoinsTip 6 | 7 | 指向活跃 CCoinsView 智能指针。 8 | 9 | -------------------------------------------------------------------------------- /keystruct/key.md: -------------------------------------------------------------------------------- 1 | # 密钥相关 2 | 3 | ## 1、私钥 4 | 5 | 系统中使用 `CKey` 来表示私钥的封装。。普通私钥长度为 279,压缩私钥长度为 214。 6 | 7 | 具有下列重要属性: 8 | 9 | - fValid 10 | 11 | 私钥是否有效 12 | 13 | - fCompressed 14 | 15 | 是否(将)压缩对应于该私钥的公钥 16 | 17 | - keydata 18 | 19 | 真实的数据 20 | 21 | 22 | ## 2、CExtKey 23 | 24 | 扩展私钥,具有下列属性: 25 | 26 | 1. nDepth 27 | 28 | 2. vchFingerprint 29 | 30 | 3. nChild 31 | 32 | 4. chaincode 33 | 34 | 5. key 35 | 36 | 37 | ## 3、公钥 38 | 39 | 系统中使用类 `CPubKey` 来表示公钥。普通公钥长度为 65,压缩公钥长度为 33。 40 | 41 | 签名长度为 72,压缩签名长度为 65。 42 | 43 | 公钥内部使用一个无符号字符数组 `vch` ,来存储序列化的数据。它的第一个字符是一个标志,如果等于 2 ,或3 那么公钥的长度就是压缩的长度,如果等于 4,或6,或7 那么公钥长度就是无压缩的长度,其他长度都是 0。 44 | 45 | `GetLen`、`size` 都是根据 `vch` 数组的第一个字符来判断的,`Invalidate` 设置公钥无效时,设置数组的第一个字符为 `0xFF` 46 | 47 | 下面对某些方法做简单说明: 48 | 49 | 1. GetID 方法 50 | 51 | 生成并返回一个 CKeyID 对象。先调用 `Hash160` 方法,计算参数 160 位的哈希,内部分别使用 `SHA256` 和 `RIPEMD160` 进行双重哈希。 52 | 53 | 2. GetHash 54 | 55 | 返回公钥 256 位的哈希,内部调用 `SHA256` 来生成哈希的。 56 | 57 | 58 | 59 | ## 4、CExtPubKey 60 | 61 | 扩展公钥,具有下列属性: 62 | 63 | - nDepth 64 | 65 | - vchFingerprint 66 | 67 | - nChild 68 | 69 | - chaincode 70 | 71 | - pubkey 72 | 73 | 74 | ## 5、CKeyID 75 | 76 | 一个私钥 `CKey` 的引用。 77 | 78 | 79 | ## 6、密钥池 80 | 81 | 密钥池由类 `CKeyPool` 表示,具有下列重要属性: 82 | 83 | - nTime 84 | 85 | - vchPubKey 86 | 87 | 一个公钥对象。生成地址时候,设置的。 88 | 89 | - fInternal 90 | 91 | - m_pre_split 92 | 93 | -------------------------------------------------------------------------------- /keystruct/keystore.md: -------------------------------------------------------------------------------- 1 | # SigningProvider 2 | 3 | 一个支持签名的接口,keystores 实现了它的接口。 4 | 5 | # CKeyStore 6 | 7 | 一个纯虚函数的基类。 8 | 9 | # CBasicKeyStore 10 | 11 | 基本的 KeyStore,继承自 `CKeyStore`,把密钥保存在 `address->secret` 映射中。 12 | 13 | mapKeys,保存私钥的映射。 14 | 15 | mapWatchKeys,保存公钥的映射。 16 | 17 | mapScripts,脚本的映射。 18 | 19 | setWatchOnly,脚本的向量。 20 | 21 | 22 | # CCryptoKeyStore 23 | 24 | 保存加密私钥的 Keystore,继承自基本的 key store。 25 | 26 | fUseCrypto 一个标志,如果为真,则 mapKeys 为空,如果为假,则 vMasterKey 为空。在构造函数中被设置为假。 27 | 28 | -------------------------------------------------------------------------------- /keystruct/params.md: -------------------------------------------------------------------------------- 1 | # 比特币关键数据结构之共识参数 2 | -------------------------------------------------------------------------------- /keystruct/signal.md: -------------------------------------------------------------------------------- 1 | # 区块验证相关 2 | 3 | 4 | ## 1、CMainSignals 5 | 6 | 主信号类,包括了一个指向结构体 `MainSignalsInstance` 的智能指针 `m_internals`,几个私有的友元方法,几个公开的方法。 7 | 8 | 这个类有一个定义在 `validationInterface.cpp` 文件中的静态全局变量 `g_signals` 在系统启动时预先进行初始化,其内部指向主信号实例 `MainSignalsInstance` 的 `m_internals` 属性在系统启动的第 4a 步中被调用 `RegisterBackgroundSignalScheduler` 方法进行初始化。 9 | 10 | 11 | 私有友元方法 12 | 13 | 1. RegisterValidationInterface 14 | 15 | 2. UnregisterValidationInterface 16 | 17 | 3. UnregisterAllValidationInterfaces 18 | 19 | 4. CallFunctionInValidationInterfaceQueue 20 | 21 | 私有方法 22 | 23 | 1. MempoolEntryRemoved 24 | 25 | 公开方法 26 | 27 | 1. RegisterBackgroundSignalScheduler 28 | 29 | 初始化类型为 `MainSignalsInstance` 结构体的内部变量 `m_internals`。 30 | 31 | 在系统启动的第 4a 步中被调用,具体请参考第 4a 步。 32 | 33 | 2. UnregisterBackgroundSignalScheduler 34 | 35 | 重置变量 `m_internals`。 36 | 37 | 3. FlushBackgroundCallbacks 38 | 39 | 4. CallbacksPending 40 | 41 | 5. RegisterWithMempoolSignals 42 | 43 | 在系统启动的第 4a 步中被调用,具体请参考第 4a 步。 44 | 45 | 6. UnregisterWithMempoolSignals 46 | 47 | 7. UpdatedBlockTip 48 | 49 | 8. TransactionAddedToMempool 50 | 51 | 9. BlockConnected 52 | 53 | 10. BlockDisconnected 54 | 55 | 11. ChainStateFlushed 56 | 57 | 12. Broadcast 58 | 59 | 13. BlockChecked 60 | 61 | 14. NewPoWValidBlock 62 | 63 | 64 | ## 3、MainSignalsInstance 65 | 66 | 结构体,定义于 `validationinterface.cpp` 文件中。主体是一些 `boost::signals2::signal` 定义的信号,具体有以下几个: 67 | 68 | 1. UpdatedBlockTip 69 | 70 | 2. TransactionAddedToMempool 71 | 72 | 3. BlockConnected 73 | 74 | 4. BlockDisconnected 75 | 76 | 5. TransactionRemovedFromMempool 77 | 78 | 6. ChainStateFlushed 79 | 80 | 7. Broadcast 81 | 82 | 8. BlockChecked 83 | 84 | 9. NewPoWValidBlock 85 | 86 | 同时,还有一个指向 `SingleThreadedSchedulerClient` 的内部变量 `m_schedulerClient`,在初始化时被设置。 87 | 88 | 89 | ## 3、CValidationInterface 90 | 91 | 实现这个类以便订阅在验证过程中产生的各种事件。 92 | 93 | 事件方法如下: 94 | 95 | 1. UpdatedBlockTip 96 | 97 | 虚函数,通知监听器区块链顶端向前了。 98 | 99 | 2. TransactionAddedToMempool 100 | 101 | 虚函数,通知监听器交易被加入到交易池中。 102 | 103 | 3. TransactionRemovedFromMempool 104 | 105 | 虚函数,通知监听器交易被从交易池移出中。 106 | 107 | 4. BlockConnected 108 | 109 | 虚函数,通知监听器区块被连接。 110 | 111 | 5. BlockDisconnected 112 | 113 | 虚函数,通知监听器区块被去掉连接。 114 | 115 | 6. ChainStateFlushed 116 | 117 | 虚函数,Notifies listeners of the new active block chain on-disk. 118 | 119 | 7. ResendWalletTransactions 120 | 121 | 虚函数,Tells listeners to broadcast their data. 122 | 123 | 8. BlockChecked 124 | 125 | 虚函数,Notifies listeners of a block validation result. 126 | 127 | 9. NewPoWValidBlock 128 | 129 | 虚函数,Notifies listeners that a block which builds directly on our current tip has been received and connected to the headers tree, though not validated yet 130 | 131 | 友元方法如下: 132 | 133 | 1. RegisterValidationInterface 134 | 135 | 注册一个验证接口/监听器。 136 | 137 | 2. UnregisterValidationInterface 138 | 139 | 删除一个验证接口/监听器。 140 | 141 | 3. UnregisterAllValidationInterfaces 142 | 143 | 删除所有验证接口/监听器。 144 | 145 | 146 | 当前的子类有:`CWallet`、`CZMQNotificationInterface`、`PeerLogicValidation`、`submitblock_StateCatcher` 等。 147 | 148 | 1. `CWallet` 实现了以下几个方法: 149 | 150 | - TransactionAddedToMempool 151 | 152 | - BlockConnected 153 | 154 | - BlockDisconnected 155 | 156 | - TransactionRemovedFromMempool 157 | 158 | - ResendWalletTransactions 159 | 160 | 161 | 2. `CZMQNotificationInterface` 实现了以下几个方法: 162 | 163 | - TransactionAddedToMempool 164 | 165 | - BlockConnected 166 | 167 | - BlockDisconnected 168 | 169 | - UpdatedBlockTip 170 | 171 | 3. `PeerLogicValidation` 实现了以下几个方法: 172 | 173 | - BlockConnected 174 | 175 | - UpdatedBlockTip 176 | 177 | - BlockChecked 178 | 179 | - NewPoWValidBlock 180 | 181 | 4. `submitblock_StateCatcher` 实现了以下几个方法: 182 | 183 | - BlockChecked 184 | -------------------------------------------------------------------------------- /keystruct/wallet.md: -------------------------------------------------------------------------------- 1 | # 钱包及地址相关结构 2 | 3 | ## 钱包 4 | 5 | **钱包是 keystore 的扩展,管理着交易和余额,提供创建交易的能力,同时还实现了 `CValidationInterface` 的某些接口**。 6 | 7 | 具有下列重要属性: 8 | 9 | - fAbortRescan 10 | 11 | 是否终止扫描 12 | 13 | - fScanningWallet 14 | 15 | 是否正在扫描 16 | 17 | - mutexScanning 18 | 19 | - WalletRescanReserver 20 | 21 | - encrypted_batch 22 | 23 | - nWalletVersion 24 | 25 | - nWalletMaxVersion 26 | 27 | - nNextResend 28 | 29 | - nLastResend 30 | 31 | - fBroadcastTransactions 32 | 33 | 是否广播交易。创建钱包时,根据启动参数 `-walletbroadcast` 设置,默认为真。 34 | 35 | - TxSpends 36 | 37 | - mapTxSpends 38 | 39 | - hdChain 40 | 41 | 确定分层链数据模型,类型为 `CHDChain` 42 | 43 | - setInternalKeyPool 44 | 45 | - setExternalKeyPool 46 | 47 | - set_pre_split_keypool 48 | 49 | - m_max_keypool_index 50 | 51 | - m_pool_key_to_index 52 | 53 | - m_wallet_flags 54 | 55 | - nTimeFirstKey 56 | 57 | - m_name 58 | 59 | - database 60 | 61 | - m_last_block_processed 62 | 63 | - mapKeyMetadata 64 | 65 | - m_script_metadata 66 | 67 | - MasterKeyMap 68 | 69 | - mapMasterKeys 70 | 71 | - nMasterKeyMaxID 72 | 73 | - mapWallet 74 | 75 | - TxItems 76 | 77 | - wtxOrdered 78 | 79 | - nOrderPosNext 80 | 81 | - nAccountingEntryNumber 82 | 83 | - mapAddressBook 84 | 85 | - setLockedCoins 86 | 87 | 88 | ## 钱包功能 89 | 90 | 系统中钱包功能由枚举类 `WalletFeature` 表示,钱包具有下面这些功能: 91 | 92 | - FEATURE_BASE 93 | 94 | 最早钱包支持的功能,仅用于 `getwalletinfo` 输出。 95 | 96 | - FEATURE_WALLETCRYPT 97 | 98 | 钱包支持加密 99 | 100 | - FEATURE_COMPRPUBKEY 101 | 102 | 钱包支持压缩公钥 103 | 104 | - FEATURE_HD 105 | 106 | BIP32 之后的分层密钥推导 HD 钱包 107 | 108 | - FEATURE_HD_SPLIT 109 | 110 | 具有 HD 链分割的钱包 111 | 112 | - FEATURE_NO_DEFAULT_KEY 113 | 114 | Wallet without a default key written 115 | 116 | - FEATURE_PRE_SPLIT_KEYPOOL 117 | 118 | Upgraded to HD SPLIT and can have a pre-split keypool 119 | 120 | - FEATURE_LATEST 121 | 122 | 等于 `FEATURE_PRE_SPLIT_KEYPOOL` 123 | 124 | ## 输出类型 125 | 126 | 输出类型由枚举类 `OutputType` 表示,具体类型有: 127 | 128 | - LEGACY 129 | 130 | - P2SH_SEGWIT 131 | 132 | 默认的地址类型 133 | 134 | - BECH32 135 | 136 | - CHANGE_AUTO 137 | 138 | ## CMerkleTx 139 | 140 | A transaction with a merkle branch linking it to the block chain. 141 | 142 | 具有下列重要的属性: 143 | 144 | - ABANDON_HASH 145 | 146 | - tx 147 | 148 | 交易对象,类型为 `CTransactionRef` 149 | 150 | - hashBlock 151 | 152 | - nIndex 153 | 154 | 155 | ## CWalletTx 156 | 157 | 继承自 `CMerkleTx`。 158 | 159 | 具有下列重要的属性: 160 | 161 | - pwallet 162 | 163 | 指向钱包的指针,类型为 `CWallet` 164 | 165 | - mapValue 166 | 167 | - vOrderForm 168 | 169 | - fTimeReceivedIsTxTime 170 | 171 | - nTimeReceived 172 | 173 | - nTimeSmart 174 | 175 | - fFromMe 176 | 177 | - nOrderPos 178 | 179 | 交易列表中的位置 180 | 181 | - m_it_wtxOrdered 182 | 183 | - fDebitCached 184 | 185 | - fCreditCached 186 | 187 | - fImmatureCreditCached 188 | 189 | - fAvailableCreditCached 190 | 191 | - fWatchDebitCached 192 | 193 | - fWatchCreditCached 194 | 195 | - fImmatureWatchCreditCached 196 | 197 | - fAvailableWatchCreditCached 198 | 199 | - fChangeCached 200 | 201 | - fInMempool 202 | 203 | - nDebitCached 204 | 205 | - nCreditCached 206 | 207 | - nImmatureCreditCached 208 | 209 | - nAvailableCreditCached 210 | 211 | - nWatchDebitCached 212 | 213 | - nWatchCreditCached 214 | 215 | - nImmatureWatchCreditCached 216 | 217 | - nAvailableWatchCreditCached 218 | 219 | - nChangeCached 220 | 221 | 222 | ## 交易输出 223 | 224 | 交易输出由类 `COutput` 表示,具有下列重要属性: 225 | 226 | - tx 227 | 228 | 指向钱包的交易,类型为 `CWalletTx` 229 | 230 | - i 231 | 232 | - nDepth 233 | 234 | - nInputBytes 235 | 236 | - fSpendable 237 | 238 | 是否有私钥可以花费这笔输出 239 | 240 | - fSolvable 241 | 242 | 是否我们知道如何花费这笔输出 243 | 244 | - use_max_sig 245 | 246 | - fSafe 247 | 248 | 这笔输出是否可以安全地花费。 249 | 250 | 251 | ## 钱包私钥 252 | 253 | 包含失效日期的钱包私钥,类型为 `CWalletKey`,具有下列重要属性: 254 | 255 | - vchPrivKey 256 | 257 | 私钥,类型为 `CPrivKey` 258 | 259 | - nTimeCreated 260 | 261 | - nTimeExpires 262 | 263 | - strComment 264 | 265 | 266 | ## CReserveKey 267 | 268 | 继承自 `CReserveScript` 269 | 270 | - pwallet 271 | 272 | - nIndex 273 | 274 | - vchPubKey 275 | 276 | - fInternal 277 | 278 | 279 | ## WalletRescanReserver 280 | 281 | - m_wallet 282 | 283 | - m_could_reserve 284 | 285 | 286 | 287 | - 288 | -------------------------------------------------------------------------------- /net/connode.md: -------------------------------------------------------------------------------- 1 | # P2P 网络建立之连接对等节点处理 2 | 3 | P2P 网络的建立是在系统启动的第 12 步,最后时刻调用 `CConnman::Start` 方法开始的。 4 | 5 | 本部分内容在 `net.cpp`、`net_processing.cpp` 等文件中。 6 | 7 | ## ThreadOpenAddedConnections 8 | 9 | 这个线程的主要作用是生成地址对象,并且调用 `OpenNetworkConnection` 方法,连接到指定地址。 10 | 11 | 线程定义在 `net.cpp` 文件的 1959 行。下面我们开始进行具体的解读。 12 | 13 | 线程的主体是一个 `while` 循环。在循环中进行下面的处理。 14 | 15 | 1. 调用 `GetAddedNodeInfo` 方法,获取所有的节点信息。 16 | 17 | 本方法返回所有的节点信息,其中即有已连接的,也有未连接的地址。 18 | 19 | - 首先,生成保存节点信息的容器变量 `ret` 和保存地址字符串的列表对象 `lAddresses`。然后把 `vAddedNodes` 集合中的所有地址拷贝到 `lAddresses` 中。 20 | 21 | std::vector ret; 22 | 23 | std::list lAddresses(0); 24 | { 25 | LOCK(cs_vAddedNodes); 26 | ret.reserve(vAddedNodes.size()); 27 | std::copy(vAddedNodes.cbegin(), vAddedNodes.cend(), std::back_inserter(lAddresses)); 28 | } 29 | 30 | - 遍历所有的节点(`vNodes` 节点容器),进行下面处理。 31 | 32 | 如果当前节点的地址是有效的,则加入 `mapConnected` map 中,Key 为当前节点的地址,值标明当前节点是否为入站节点。 33 | 34 | 获取当前节点的地址名称。如果名称不空,则放进 `mapConnectedByName` map 中,Key 为当前节点的地址名称,值为一个 `std::pair` 对象,其中第一个值表明当前节点是否为入站节点,第二个值为节点的地址。 35 | 36 | std::map mapConnected; 37 | std::map> mapConnectedByName; 38 | { 39 | LOCK(cs_vNodes); 40 | for (const CNode* pnode : vNodes) { 41 | if (pnode->addr.IsValid()) { 42 | mapConnected[pnode->addr] = pnode->fInbound; 43 | } 44 | std::string addrName = pnode->GetAddrName(); 45 | if (!addrName.empty()) { 46 | mapConnectedByName[std::move(addrName)] = std::make_pair(pnode->fInbound, static_cast(pnode->addr)); 47 | } 48 | } 49 | } 50 | 51 | - 遍历 `lAddresses` 变量,进行下面处理。 52 | 53 | 根据当前地址和当前网络类型,生成一个 `service` 对象,类型为 `CService`,和一个节点信息对象。 54 | 55 | 如果当前地址是 IP:Port 形式,那么查找 `mapConnected` 集合对应的地址。如果可以找到,则设置节点信息对象的相关属性。 56 | 57 | 如果当前地址是名称的形式,那么查找 `mapConnectedByName` 集合对应的地址。如果可以找到,则设置节点信息对象的相关属性。 58 | 59 | 把当前地址信息对象加入 `ret` 集合中。 60 | 61 | for (const std::string& strAddNode : lAddresses) { 62 | CService service(LookupNumeric(strAddNode.c_str(), Params().GetDefaultPort())); 63 | AddedNodeInfo addedNode{strAddNode, CService(), false, false}; 64 | if (service.IsValid()) { 65 | // strAddNode is an IP:port 66 | auto it = mapConnected.find(service); 67 | if (it != mapConnected.end()) { 68 | addedNode.resolvedAddress = service; 69 | addedNode.fConnected = true; 70 | addedNode.fInbound = it->second; 71 | } 72 | } else { 73 | // strAddNode is a name 74 | auto it = mapConnectedByName.find(strAddNode); 75 | if (it != mapConnectedByName.end()) { 76 | addedNode.resolvedAddress = it->second.second; 77 | addedNode.fConnected = true; 78 | addedNode.fInbound = it->second.first; 79 | } 80 | } 81 | ret.emplace_back(std::move(addedNode)); 82 | } 83 | 84 | - 返回`ret` 集合。 85 | 86 | 2. 遍历所有的节点信息,如果当前节点还没有连接,进行下面的处理: 87 | 88 | 生成地址对象 `addr`,类型为 `CAddress`。 89 | 90 | 调用 `OpenNetworkConnection` 方法,连接到当前的节点。 91 | 92 | for (const AddedNodeInfo& info : vInfo) { 93 | if (!info.fConnected) { 94 | if (!grant.TryAcquire()) { 95 | // If we've used up our semaphore and need a new one, let's not wait here since while we are waiting 96 | // the addednodeinfo state might change. 97 | break; 98 | } 99 | tried = true; 100 | CAddress addr(CService(), NODE_NONE); 101 | OpenNetworkConnection(addr, false, &grant, info.strAddedNode.c_str(), false, false, true); 102 | if (!interruptNet.sleep_for(std::chrono::milliseconds(500))) 103 | return; 104 | } 105 | } 106 | 107 | 108 | 下面我们具体看下 `OpenNetworkConnection` 函数的处理。 109 | 110 | 1. 如果 `interruptNet` 为真,则返回。如果网络没有激活(`fNetworkActive` 为假),则返回。 111 | 112 | if (interruptNet) { 113 | return; 114 | } 115 | if (!fNetworkActive) { 116 | return; 117 | } 118 | 119 | 2. 如果参数 `pszDest` 为空(当前节点信息的地址),进一步,如果要连接的节点是本地的,或是已连接的,或是禁止的,则返回。如果参数 `pszDest` 不为空,进一步,如果节点是已连接的,则返回。 120 | 121 | if (!pszDest) { 122 | if (IsLocal(addrConnect) || 123 | FindNode(static_cast(addrConnect)) || IsBanned(addrConnect) || 124 | FindNode(addrConnect.ToStringIPPort())) 125 | return; 126 | } else if (FindNode(std::string(pszDest))) 127 | return; 128 | 129 | 3. **调用 `ConnectNode` 方法,连接到指定地址,并返回对等节点 `CNode` 对象**。如果连接失败,则返回。 130 | 131 | 4. 如果参数 `grantOutbound` 对象存在,则调用其 `MoveTo` 方法,进行处理。 132 | 133 | 5. 如果参数 `fOneShot` 为真,则设置对等节点的 `fOneShot` 属性为真。 134 | 135 | 6. 如果是临时探测节点(参数`fFeeler` 为真),则设置对等节点的 `fFeeler` 属性为真。 136 | 137 | 7. 如果是手动连接的,则设置对等节点的 `m_manual_connection` 属性为真。 138 | 139 | 8. **调用网络事件处理器的 `InitializeNode` 方法,进行对等节点初始化。** 140 | 141 | 具体代码在 `net_processing.cpp` 文件的第 611 行,如下所示: 142 | 143 | void PeerLogicValidation::InitializeNode(CNode *pnode) { 144 | CAddress addr = pnode->addr; 145 | std::string addrName = pnode->GetAddrName(); 146 | NodeId nodeid = pnode->GetId(); 147 | { 148 | LOCK(cs_main); 149 | mapNodeState.emplace_hint(mapNodeState.end(), std::piecewise_construct, std::forward_as_tuple(nodeid), std::forward_as_tuple(addr, std::move(addrName))); 150 | } 151 | if(!pnode->fInbound) 152 | PushNodeVersion(pnode, connman, GetTime()); 153 | } 154 | 155 | 代码最主要的动作是,检查节点是否为出站节点,即连接到别的对等节点,如果是则调用 `PushNodeVersion` 方法,发送版本信息。具体消息消息处理部分。 156 | 157 | 9. **把生成的对等节点保存到 `vNodes` 向量中**。 158 | 159 | 160 | ### 3.1、ConnectNode 161 | 162 | 这个方法负责连接到具体的对等节点。我们来看下具体的处理。 163 | 164 | 1. 如果参数 `pszDest` 为空指针,则处理如下: 165 | 166 | 如果要连接的地址是本地地址,则直接返回空指针。调用 `FindNode` 方法,查看指定的节点是否存在。如果存在,即已经连接,则返回空指针。 167 | 168 | if (pszDest == nullptr) { 169 | if (IsLocal(addrConnect)) 170 | return nullptr; 171 | // Look for an existing connection 172 | CNode* pnode = FindNode(static_cast(addrConnect)); 173 | if (pnode) 174 | { 175 | LogPrintf("Failed to open new connection, already connected\n"); 176 | return nullptr; 177 | } 178 | } 179 | 180 | 2. 如果参数 `pszDest` 不是空指针,那么调用 `Lookup` 方法,查找/生成地址字符串对应的地址对象。如果找到,则进行下面的处理: 181 | 182 | 生成要连接的地址对象。如果地址地址对象是无效的,则返回空指针。调用 `FindNode` 方法,查找对应的地址对象。如果存在,即已经连接,则返回空指针。 183 | 184 | 这个地方解析要连接的地址字符串生成要连接的地址对象。 185 | 186 | const int default_port = Params().GetDefaultPort(); 187 | if (pszDest) { 188 | std::vector resolved; 189 | if (Lookup(pszDest, resolved, default_port, fNameLookup && !HaveNameProxy(), 256) && !resolved.empty()) { 190 | addrConnect = CAddress(resolved[GetRand(resolved.size())], NODE_NONE); 191 | if (!addrConnect.IsValid()) { 192 | LogPrint(BCLog::NET, "Resolver returned invalid address %s for %s\n", addrConnect.ToString(), pszDest); 193 | return nullptr; 194 | } 195 | LOCK(cs_vNodes); 196 | CNode* pnode = FindNode(static_cast(addrConnect)); 197 | if (pnode) 198 | { 199 | pnode->MaybeSetAddrName(std::string(pszDest)); 200 | LogPrintf("Failed to open new connection, already connected\n"); 201 | return nullptr; 202 | } 203 | } 204 | } 205 | 206 | 3. 如果要连接的地址对象是有效的,进行下面的处理。 207 | 208 | 调用 `GetProxy` 方法,返回代理类型。如果方法返回为真,即存在代理,那么调用 `CreateSocket` 方法,创建代理套接字。如果成功创建,调用 `ConnectThroughProxy` 方法,通过代理连接到对等节点。 209 | 210 | 如果不存在代理,那么调用 `CreateSocket` 方法,创建对等节点的套接字。如果成功创建,调用 `ConnectSocketDirectly` 方法,直接连接到对等节点。 211 | 212 | bool proxyConnectionFailed = false; 213 | 214 | if (GetProxy(addrConnect.GetNetwork(), proxy)) { 215 | hSocket = CreateSocket(proxy.proxy); 216 | if (hSocket == INVALID_SOCKET) { 217 | return nullptr; 218 | } 219 | connected = ConnectThroughProxy(proxy, addrConnect.ToStringIP(), addrConnect.GetPort(), hSocket, nConnectTimeout, &proxyConnectionFailed); 220 | } else { 221 | // no proxy needed (none set for target network) 222 | hSocket = CreateSocket(addrConnect); 223 | if (hSocket == INVALID_SOCKET) { 224 | return nullptr; 225 | } 226 | connected = ConnectSocketDirectly(addrConnect, hSocket, nConnectTimeout, manual_connection); 227 | } 228 | if (!proxyConnectionFailed) { 229 | // If a connection to the node was attempted, and failure (if any) is not caused by a problem connecting to 230 | // the proxy, mark this as an attempt. 231 | addrman.Attempt(addrConnect, fCountFailure); 232 | } 233 | 234 | 4. 如果要连接的字符串不空,且存在代理,那么: 235 | 236 | 调用 `CreateSocket` 方法,生成代理的套接字。然后,调用 `ConnectThroughProxy` 方法,通过代理连接到指定的对等节点。 237 | 238 | hSocket = CreateSocket(proxy.proxy); 239 | if (hSocket == INVALID_SOCKET) { 240 | return nullptr; 241 | } 242 | std::string host; 243 | int port = default_port; 244 | SplitHostPort(std::string(pszDest), port, host); 245 | connected = ConnectThroughProxy(proxy, host, port, hSocket, nConnectTimeout, nullptr); 246 | 247 | 5. 如果以上都没有连接到主节点,则关闭套接字并返回空指针。 248 | 249 | if (!connected) { 250 | CloseSocket(hSocket); 251 | return nullptr; 252 | } 253 | 254 | 6. 最后,生成并返回主节点对象。 255 | 256 | NodeId id = GetNewNodeId(); 257 | uint64_t nonce = GetDeterministicRandomizer(RANDOMIZER_ID_LOCALHOSTNONCE).Write(id).Finalize(); 258 | CAddress addr_bind = GetBindAddress(hSocket); 259 | CNode* pnode = new CNode(id, nLocalServices, GetBestHeight(), hSocket, addrConnect, CalculateKeyedNetGroup(addrConnect), nonce, addr_bind, pszDest ? pszDest : "", false); 260 | pnode->AddRef(); 261 | return pnode; 262 | -------------------------------------------------------------------------------- /net/dnsseed.md: -------------------------------------------------------------------------------- 1 | # P2P 网络建立之 DNS 种子节点处理 2 | 3 | P2P 网络的建立是在系统启动的第 12 步,最后时刻调用 `CConnman::Start` 方法开始的。 4 | 5 | 本部分内容在 `net.cpp`、`net_processing.cpp` 等文件中。 6 | 7 | 8 | ## ThreadDNSAddressSeed 9 | 10 | 这个线程的目标是,只有在需要地址时才查询 DNS 种子,当我们不需要 DNS 种子时,会避免 DNS 种子查询。这样可以通过创建更少的识别 DNS 请求来提高用户隐私,通过减少种子对网络拓扑的影响来减少信任,并减少种子的流量。 11 | 12 | 线程定义在 `net.cpp` 文件的 1603 行。下面我们开始进行具体的解读。 13 | 14 | 1. 如果对等节点的数量大于 0,且没有指定 `-forcednsseed`,或指定了但值为 `false`,进行下面的处理: 15 | 16 | 遍历所有的节点,如果节点已成功连接,且不是引导节点,且 `fOneShot` 属性为假,且不是不是手动连接的,且不是入站节点,那么变量 `nRelevant` 加1。 17 | 18 | 如果变量 `nRelevant` 大于2,即 P2P 网络已经可用,则退出函数。 19 | 20 | if ((addrman.size() > 0) && 21 | (!gArgs.GetBoolArg("-forcednsseed", DEFAULT_FORCEDNSSEED))) { 22 | if (!interruptNet.sleep_for(std::chrono::seconds(11))) 23 | return; 24 | 25 | LOCK(cs_vNodes); 26 | int nRelevant = 0; 27 | for (auto pnode : vNodes) { 28 | nRelevant += pnode->fSuccessfullyConnected && !pnode->fFeeler && !pnode->fOneShot && !pnode->m_manual_connection && !pnode->fInbound; 29 | } 30 | if (nRelevant >= 2) { 31 | LogPrintf("P2P peers available. Skipped DNS seeding.\n"); 32 | return; 33 | } 34 | } 35 | 36 | 2. 获取并遍历所有的 DNS 种子节点。 37 | 38 | for (const std::string &seed : vSeeds) { 39 | if (interruptNet) { 40 | return; 41 | } 42 | if (HaveNameProxy()) { 43 | AddOneShot(seed); 44 | } else { 45 | std::vector vIPs; 46 | std::vector vAdd; 47 | ServiceFlags requiredServiceBits = GetDesirableServiceFlags(NODE_NONE); 48 | std::string host = strprintf("x%x.%s", requiredServiceBits, seed); 49 | CNetAddr resolveSource; 50 | if (!resolveSource.SetInternal(host)) { 51 | continue; 52 | } 53 | unsigned int nMaxIPs = 256; // Limits number of IPs learned from a DNS seed 54 | if (LookupHost(host.c_str(), vIPs, nMaxIPs, true)) 55 | { 56 | for (const CNetAddr& ip : vIPs) 57 | { 58 | int nOneDay = 24*3600; 59 | CAddress addr = CAddress(CService(ip, Params().GetDefaultPort()), requiredServiceBits); 60 | addr.nTime = GetTime() - 3*nOneDay - GetRand(4*nOneDay); // use a random age between 3 and 7 days old 61 | vAdd.push_back(addr); 62 | found++; 63 | } 64 | addrman.Add(vAdd, resolveSource); 65 | } else { 66 | // We now avoid directly using results from DNS Seeds which do not support service bit filtering, 67 | // instead using them as a oneshot to get nodes with our desired service bits. 68 | AddOneShot(seed); 69 | } 70 | } 71 | } 72 | 73 | 下面,对上面的代码进行讲解。 74 | 75 | 如果指定了代理,则调用 `AddOneShot` 方法,保存当前 DNS 种子节点到 `vOneShots` 集合中。否则,进行下面的处理: 76 | 77 | - 生成两个集合 `vIPs`、`vAdd` 。`vIPs` 集合中保存的是 `CNetAddr` 对象,代表了一个IP地址。`vAdd`集合中保存的是 `CAddress` 对象,`CAddress` 继承自 `CService`,后者又继承自 `CAddress`,包含了一些关于对等节点别的信息。 78 | 79 | - 调用 `GetDesirableServiceFlags` 方法,获得服务标志位。 80 | 81 | - 调用 `strprintf` 函数,格式化 DNS 种子节点的地址。 82 | 83 | `strprintf` 是一个宏定义,实际调用的是 Boost 库的 `tfm::format`。 84 | 85 | - 生成类型为 `CNetAddr` 的地址对象 `resolveSource`,并调用其 `SetInternal` 方法,设置 `resolveSource` 的 IP。如果出错,则返回处理下一个。 86 | 87 | - 调用 `LookupHost` 方法,根据 DNS 种子节点获取其保存的对等节点列表。并保存在 `vIPs` 集合中。 88 | 89 | `LookupHost` 方法内部主要调用了 `LookupIntern` 方法进行处理。下面我们看下后者的具体处理。 90 | 91 | - 生成一个地址对象 `addr`。然后调用其 `SetSpecial` 方法进行处理。 92 | 93 | 在该方法内部,如果 DNS种子节点不是以 `.onion` 结尾,即不是暗网地址,则直接返回假。否则进行下面的处理。 94 | 95 | 调用 `DecodeBase32` 方法,解析不包括暗网后缀在内的具体的地址。接下来,检查地址的长度是否不等于指定的长度,如果是则返回假。否则,对地址进行处理并转化为IP地址,然后返回真。 96 | 97 | bool CNetAddr::SetSpecial(const std::string &strName) 98 | { 99 | if (strName.size()>6 && strName.substr(strName.size() - 6, 6) == ".onion") { 100 | std::vector vchAddr = DecodeBase32(strName.substr(0, strName.size() - 6).c_str()); 101 | if (vchAddr.size() != 16-sizeof(pchOnionCat)) 102 | return false; 103 | memcpy(ip, pchOnionCat, sizeof(pchOnionCat)); 104 | for (unsigned int i=0; i<16-sizeof(pchOnionCat); i++) 105 | ip[i + sizeof(pchOnionCat)] = vchAddr[i]; 106 | return true; 107 | } 108 | return false; 109 | } 110 | 111 | 如果前面方法返回的结果为真,即 DNS 种子为暗网地址,则把当前地址加入 `vIP` 集合,并返回。 112 | 113 | CNetAddr addr; 114 | if (addr.SetSpecial(std::string(pszName))) { 115 | vIP.push_back(addr); 116 | return true; 117 | } 118 | 119 | - 生成一个类型为 `addrinfo` 的结构体对象 aiHint,并设置其各个属性值。 120 | 121 | - 生成一个类型为 `addrinfo` 的结构体对象 aiRes,然后调用 `getaddrinfo` 方法,根据 DNS 种子节点来获取一个地址链接表。 122 | 123 | 这个方法是系统提供的方法,返回的是一个 sockaddr 结构的链表而不是一个地址清单。第一个参数是一个主机名或者地址串,第二个参数是一个服务名或者10进制端口号数串,第三个参数可以是一个空指针,也可以是一个指向某个addrinfo结构的指针,调用者在这个结构中填入关于期望返回的信息类型的暗示,最后一个参数是返回的结果。 124 | 125 | int nErr = getaddrinfo(pszName, nullptr, &aiHint, &aiRes); 126 | if (nErr) 127 | return false; 128 | 129 | - 接下来只要地址信息链表不空,且当前获取的对等节点IP数量小于指定的数量或者指定的数量是0(即不限制对等节点的数量),就循环这个链表进行下面的处理。 130 | 131 | 根据返回的地址信息对象,是IPV4 或者是 IPV6,生成生成不同的 `CNetAddr` 对象。如果这个地址对象不是内部 IP,则保存到 `vIP` 集合中。从地址信息链表中取得下一个地址信息对象。 132 | 133 | struct addrinfo *aiTrav = aiRes; 134 | while (aiTrav != nullptr && (nMaxSolutions == 0 || vIP.size() < nMaxSolutions)) 135 | { 136 | CNetAddr resolved; 137 | if (aiTrav->ai_family == AF_INET) 138 | { 139 | assert(aiTrav->ai_addrlen >= sizeof(sockaddr_in)); 140 | resolved = CNetAddr(((struct sockaddr_in*)(aiTrav->ai_addr))->sin_addr); 141 | } 142 | 143 | if (aiTrav->ai_family == AF_INET6) 144 | { 145 | assert(aiTrav->ai_addrlen >= sizeof(sockaddr_in6)); 146 | struct sockaddr_in6* s6 = (struct sockaddr_in6*) aiTrav->ai_addr; 147 | resolved = CNetAddr(s6->sin6_addr, s6->sin6_scope_id); 148 | } 149 | /* Never allow resolving to an internal address. Consider any such result invalid */ 150 | if (!resolved.IsInternal()) { 151 | vIP.push_back(resolved); 152 | } 153 | 154 | aiTrav = aiTrav->ai_next; 155 | } 156 | 157 | - 调用 `freeaddrinfo` 方法,释放 `getaddrinfo` 方法所申请的内存空间。 158 | 159 | - 根据 `vIP` 集合的大小,返回真假。 160 | 161 | - 如果 `LookupHost` 方法返回结果为真,即根据当前 DNS 种子节点查找到了至少一个对等节点,则进行下面的处理。 162 | 163 | 遍历 `vIPs` 集合,根据当前的 IP 地址,生成一个 `CAddress` 地址对象,并保存在 `vAdd` 集合中,同时把代表找到节点的变量 `found` 加1。 164 | 165 | 调用地址管理器的 `Add` 方法,保存多个地址。 166 | 167 | 具体代码如下: 168 | 169 | for (const CNetAddr& ip : vIPs) 170 | { 171 | int nOneDay = 24*3600; 172 | CAddress addr = CAddress(CService(ip, Params().GetDefaultPort()), requiredServiceBits); 173 | addr.nTime = GetTime() - 3*nOneDay - GetRand(4*nOneDay); // use a random age between 3 and 7 days old 174 | vAdd.push_back(addr); 175 | found++; 176 | } 177 | addrman.Add(vAdd, resolveSource); 178 | 179 | - 如果 `LookupHost` 方法返回结果为假,即根据当前 DNS 种子节点没找到一个对等节点,则调用 `AddOneShot` 方法进行处理。 180 | 181 | `AddOneShot` 方法内部简单地把当前 DNS 种子加入 `vOneShots` 集合。 182 | 183 | 184 | ### 2.1、CAddrMan::Add 方法 185 | 186 | 下面我们对地址管理器的 `Add` 方法做下介绍。这个方法位于 `addrman.h` 文件的 540 行。 187 | 188 | 这个方法主体是一个 for 循环,遍历 `CAddress` 集合,针对每一个 `CAddress` 对象调用 `Add_` 方法进行处理。并返回是否添加成功。代码如下: 189 | 190 | bool Add(const std::vector &vAddr, const CNetAddr& source, int64_t nTimePenalty = 0) 191 | { 192 | LOCK(cs); 193 | int nAdd = 0; 194 | Check(); 195 | for (std::vector::const_iterator it = vAddr.begin(); it != vAddr.end(); it++) 196 | nAdd += Add_(*it, source, nTimePenalty) ? 1 : 0; 197 | Check(); 198 | if (nAdd) { 199 | LogPrint(BCLog::ADDRMAN, "Added %i addresses from %s: %i tried, %i new\n", nAdd, source.ToString(), nTried, nNew); 200 | } 201 | return nAdd > 0; 202 | } 203 | 204 | 接下来,我们来看一下 `Add_` 方法。这个方法在 `addrman.cpp` 文件的第254行。 205 | 206 | 1. 如果当前地址是不可路由的,则直接返回假。 207 | 208 | if (!addr.IsRoutable()) 209 | return false; 210 | 211 | 2. 调用 `Find` 方法,根据地址对象找到其对应的地址信息。 212 | 213 | std::map::iterator it = mapAddr.find(addr); 214 | if (it == mapAddr.end()) 215 | return nullptr; 216 | if (pnId) 217 | *pnId = (*it).second; 218 | std::map::iterator it2 = mapInfo.find((*it).second); 219 | if (it2 != mapInfo.end()) 220 | return &(*it2).second; 221 | return nullptr; 222 | 223 | 3. 如果地址对象来源对象,设置变量 `nTimePenalty` 等于0。 224 | 225 | 4. 如果找到对应的地址信息,则设置地址信息的相关属性 226 | 227 | bool fCurrentlyOnline = (GetAdjustedTime() - addr.nTime < 24 * 60 * 60); 228 | int64_t nUpdateInterval = (fCurrentlyOnline ? 60 * 60 : 24 * 60 * 60); 229 | if (addr.nTime && (!pinfo->nTime || pinfo->nTime < addr.nTime - nUpdateInterval - nTimePenalty)) 230 | pinfo->nTime = std::max((int64_t)0, addr.nTime - nTimePenalty); 231 | 232 | // add services 233 | pinfo->nServices = ServiceFlags(pinfo->nServices | addr.nServices); 234 | 235 | // do not update if no new information is present 236 | if (!addr.nTime || (pinfo->nTime && addr.nTime <= pinfo->nTime)) 237 | return false; 238 | 239 | // do not update if the entry was already in the "tried" table 240 | if (pinfo->fInTried) 241 | return false; 242 | 243 | // do not update if the max reference count is reached 244 | if (pinfo->nRefCount == ADDRMAN_NEW_BUCKETS_PER_ADDRESS) 245 | return false; 246 | 247 | // stochastic test: previous nRefCount == N: 2^N times harder to increase it 248 | int nFactor = 1; 249 | for (int n = 0; n < pinfo->nRefCount; n++) 250 | nFactor *= 2; 251 | if (nFactor > 1 && (RandomInt(nFactor) != 0)) 252 | return false; 253 | 254 | 5. 如果没有找到对应的地址信息,则生成新的地址信息。 255 | 256 | pinfo = Create(addr, source, &nId); 257 | pinfo->nTime = std::max((int64_t)0, (int64_t)pinfo->nTime - nTimePenalty); 258 | nNew++; 259 | fNew = true; 260 | 261 | 在 `Create` 方法中,生成一个新的 `CAddrInfo` 对象,并放到 `mapInfo` 集合中,同时在在 `mapAddr` 集合中增加对应的条目。具体代码如下: 262 | 263 | int nId = nIdCount++; 264 | mapInfo[nId] = CAddrInfo(addr, addrSource); 265 | mapAddr[addr] = nId; 266 | mapInfo[nId].nRandomPos = vRandom.size(); 267 | vRandom.push_back(nId); 268 | if (pnId) 269 | *pnId = nId; 270 | return &mapInfo[nId]; 271 | 272 | 6. 接下来处理其他一些信息,代码比较简单不详述。 273 | 274 | int nUBucket = pinfo->GetNewBucket(nKey, source); 275 | int nUBucketPos = pinfo->GetBucketPosition(nKey, true, nUBucket); 276 | if (vvNew[nUBucket][nUBucketPos] != nId) { 277 | bool fInsert = vvNew[nUBucket][nUBucketPos] == -1; 278 | if (!fInsert) { 279 | CAddrInfo& infoExisting = mapInfo[vvNew[nUBucket][nUBucketPos]]; 280 | if (infoExisting.IsTerrible() || (infoExisting.nRefCount > 1 && pinfo->nRefCount == 0)) { 281 | // Overwrite the existing new table entry. 282 | fInsert = true; 283 | } 284 | } 285 | if (fInsert) { 286 | ClearNew(nUBucket, nUBucketPos); 287 | pinfo->nRefCount++; 288 | vvNew[nUBucket][nUBucketPos] = nId; 289 | } else { 290 | if (pinfo->nRefCount == 0) { 291 | Delete(nId); 292 | } 293 | } 294 | } 295 | 296 | 7. 返回真。 297 | -------------------------------------------------------------------------------- /net/socket.md: -------------------------------------------------------------------------------- 1 | # P2P 网络建立之套接字处理 2 | 3 | P2P 网络的建立是在系统启动的第 12 步,最后时刻调用 `CConnman::Start` 方法开始的。 4 | 5 | 本部分内容在 `net.cpp`、`net_processing.cpp` 等文件中。 6 | 7 | 8 | ## ThreadSocketHandler 9 | 10 | 这个线程主要用来处理套接字的读取和发送,调用系统提供的相关相关底层函数进行处理,把收到的消息转化成 `CNetMessage` 类型的对象,并保存到节点的 `vRecvMsg` 集合中,把待发送的消息从 `vSendMsg` 集合中取出来进行发送。 11 | 12 | 线程定义在 `net.cpp` 文件的 1155 行。下面我们开始进行具体的解读。 13 | 14 | 这个方法的主体是一个 while 循环,只要 interruptNet 变量为空,就一直循环。 15 | 16 | 1. 如果布尔变量 fNetworkActive 为假,则断开所有已经连接的接点。 17 | 18 | 遍历所有的节点列表,把没有断开的节点设置为断开连接。 19 | 20 | if (!fNetworkActive) { 21 | // Disconnect any connected nodes 22 | for (CNode* pnode : vNodes) { 23 | if (!pnode->fDisconnect) { 24 | LogPrint(BCLog::NET, "Network not active, dropping peer=%d\n", pnode->GetId()); 25 | pnode->fDisconnect = true; 26 | } 27 | } 28 | } 29 | 30 | 2. 接下来,断开所有未使用的节点。 31 | 32 | 遍历所有的节点列表,如果节点已经断开连接,则进行下面的处理: 33 | 34 | - 首先,从 vNodes 集合中删除当前节点。 35 | 36 | - 然后,调用 `CloseSocketDisconnect` 方法,关闭当前节点的套接字并进行清理。 37 | 38 | 方法内部设置 `fDisconnect` 变量为真;如果当前套接字有效,则调用 `CloseSocket` 方法,关闭套接字。`CloseSocket` 方法针对 win32 系统调用 `closesocket` 方法,别的系统调用 `close` 来关闭套接字,然后设置套接字为 `INVALID_SOCKET`。 39 | 40 | 具体方法如下所示: 41 | 42 | void CNode::CloseSocketDisconnect() 43 | { 44 | fDisconnect = true; 45 | LOCK(cs_hSocket); 46 | if (hSocket != INVALID_SOCKET) 47 | { 48 | LogPrint(BCLog::NET, "disconnecting peer=%d\n", id); 49 | CloseSocket(hSocket); 50 | } 51 | } 52 | 53 | - 再然后,调用 `Release` 方法,把节点的引用数量 `nRefCount` 减一。 54 | 55 | - 最后,把当前节点放入 vNodesDisconnected 集合中。 56 | 57 | 3. 删除所有已断开连接的节点。 58 | 59 | 遍历所有已经断开连接的节点的集合 `vNodesDisconnected`,如果当前节点的引用数量 `nRefCount` 小于等于0,进行下面的处理: 60 | 61 | - 调用宏 `TRY_LOCK` 获取 `cs_inventory` 锁。如果可以获取,则获取 `cs_vSend` 锁。如果可以获取,则设置变量 `fDelete` 为真。 62 | 63 | - 如果变量 `fDelete` 为真,则从 `vNodesDisconnected` 集合中移除当前的节点,然后调用 `DeleteNode` 方法,删除节点。 64 | 65 | 下面讲解下 `DeleteNode` 方法。方法内部首先设置变量 `fUpdateConnectionTime` 为真,然后调用消息处理接口的 `NetEventsInterface` 的 `FinalizeNode` 方法,最终处理节点。如果变量 `fUpdateConnectionTime` 在 `FinalizeNode` 方法中被设置为真,则调用地址管理器对象的 `Connected` 方法,进行处理。最后,删除当前节点。 66 | 67 | 代码如下: 68 | 69 | void CConnman::DeleteNode(CNode* pnode) 70 | { 71 | assert(pnode); 72 | bool fUpdateConnectionTime = false; 73 | m_msgproc->FinalizeNode(pnode->GetId(), fUpdateConnectionTime); 74 | if(fUpdateConnectionTime) { 75 | addrman.Connected(pnode->addr); 76 | } 77 | delete pnode; 78 | } 79 | 80 | `FinalizeNode` 方法是一个纯虚函数,具体由 `net_processing.cpp` 文件中的 `PeerLogicValidation` 对象的 `FinalizeNode` 方法实现。下面,我们来看这个函数的处理。 81 | 82 | - 设置参数 `fUpdateConnectionTime` 默认为真。 83 | 84 | - 调用 `State` 方法,获取节点状态对象的指针。 85 | 86 | 方法内部根据节点 ID,从 `mapNodeState` 集合中找到并返回对应的节点状态对象。如果不存在,则返回空指针。 87 | 88 | - 如果我们已经在这个对等节点上同步了区块头部,即节点状态对象的 `fSyncStarted` 属性为真,则设置变量 `nSyncStarted` 减一。 89 | 90 | - 如果对等节点的不良积分即节点状态对象的 `nMisbehavior` 属性等于0,且对等节点已经建立了完整的连接即节点状态对象的 `fCurrentlyConnected` 属性为真,则设置变量 `fUpdateConnectionTime` 为真。 91 | 92 | - 遍历状态对象的 `vBlocksInFlight` 集合,并删除所有的条目。 93 | 94 | - 调用 `EraseOrphansFor` 方法,删除与这个对等节点相关联的孤儿交易。 95 | 96 | 方法内部遍历 `mapOrphanTransactions` 集合,如果当前孤儿交易的来自于指定的对等节点,则调用 `EraseOrphanTx` 方法进行删除。 97 | 98 | 后者根据交易的哈希ID,查找 `mapOrphanTransactions` 集合对应的孤儿交易。如果找不到,则返回。如果找到则遍历交易的所有输入,如果当前输入指向的父输出不在 `mapOrphanTransactionsByPrev` 集合中,则处理下一个;否则,在返回的集合中移除指定的孤儿交易,如果指向的父输出现在为空,则从 `mapOrphanTransactionsByPrev` 集合中删除指向的父输出。从 `mapOrphanTransactions` 删除指定的孤儿交易。 99 | 100 | int static EraseOrphanTx(uint256 hash) EXCLUSIVE_LOCKS_REQUIRED(g_cs_orphans) 101 | { 102 | std::map::iterator it = mapOrphanTransactions.find(hash); 103 | if (it == mapOrphanTransactions.end()) 104 | return 0; 105 | for (const CTxIn& txin : it->second.tx->vin) 106 | { 107 | std::set::iterator, IteratorComparator> 108 | 109 | auto itPrev = mapOrphanTransactionsByPrev.find(txin.prevout); 110 | if (itPrev == mapOrphanTransactionsByPrev.end()) 111 | continue; 112 | itPrev->second.erase(it); 113 | if (itPrev->second.empty()) 114 | mapOrphanTransactionsByPrev.erase(itPrev); 115 | } 116 | mapOrphanTransactions.erase(it); 117 | return 1; 118 | } 119 | 120 | - 把优先下载区块的对等节点变量`nPreferredDownload` 减去状态对象的对应的 `fPreferredDownload` 属性。这个属性是一个布尔值,表示节点是否为优先下载区块。这里利用了 C++ 布尔值自动转换为整数值,真值转换为1,假值转换为0. 121 | 122 | - 处理正在下载区块的节点数量 `nPeersWithValidatedDownloads` 变量。如果节点状态对象的 `nBlocksInFlightValidHeaders` 属性不等于0,则正在下载区块的节点数量减去1,否则减去0。 123 | 124 | - 接下来处理 `g_outbound_peers_with_protect_from_disconnect` 变量,这个变量代表了出站的对等节点数量。代码比较简单,不解释。 125 | 126 | - 从节点状态集合 `mapNodeState` 中删除指定的节点ID。 127 | 128 | 4. 如果节点数量不等于 `nPrevNodeCount`,则把 `nPrevNodeCount` 设置为当前的节点数量。 129 | 130 | { 131 | LOCK(cs_vNodes); 132 | vNodesSize = vNodes.size(); 133 | } 134 | if(vNodesSize != nPrevNodeCount) { 135 | nPrevNodeCount = vNodesSize; 136 | if(clientInterface) 137 | clientInterface->NotifyNumConnectionsChanged(nPrevNodeCount); 138 | } 139 | 140 | 5. 接下来检查哪个套接字有数据要接收。 141 | 142 | 生成相关的时间变量和 3 个 fd_set 集合来处理接收数据、发送数据及数据错误,然后将集合初始化为空集。 143 | 144 | struct timeval timeout; 145 | timeout.tv_sec = 0; 146 | timeout.tv_usec = 50000; // frequency to poll pnode->vSend 147 | 148 | fd_set fdsetRecv; 149 | fd_set fdsetSend; 150 | fd_set fdsetError; 151 | FD_ZERO(&fdsetRecv); 152 | FD_ZERO(&fdsetSend); 153 | FD_ZERO(&fdsetError); 154 | 155 | 遍历当前处于监听状态的套接字 `vhListenSocket` 集合,调用 `FD_SET` 方法,把当前套接字保存在 `fdsetRecv` 集合中,然后调用标准库的 `max` 方法保存最大的套接字,设置变量 `have_fds` 为真。 156 | 157 | for (const ListenSocket& hListenSocket : vhListenSocket) { 158 | FD_SET(hListenSocket.socket, &fdsetRecv); 159 | hSocketMax = std::max(hSocketMax, hListenSocket.socket); 160 | have_fds = true; 161 | } 162 | 163 | 遍历所有的节点,设置相关的变量。 164 | 165 | for (CNode* pnode : vNodes) 166 | { 167 | bool select_recv = !pnode->fPauseRecv; 168 | bool select_send; 169 | { 170 | LOCK(pnode->cs_vSend); 171 | select_send = !pnode->vSendMsg.empty(); 172 | } 173 | 174 | LOCK(pnode->cs_hSocket); 175 | if (pnode->hSocket == INVALID_SOCKET) 176 | continue; 177 | 178 | FD_SET(pnode->hSocket, &fdsetError); 179 | hSocketMax = std::max(hSocketMax, pnode->hSocket); 180 | have_fds = true; 181 | 182 | if (select_send) { 183 | FD_SET(pnode->hSocket, &fdsetSend); 184 | continue; 185 | } 186 | if (select_recv) { 187 | FD_SET(pnode->hSocket, &fdsetRecv); 188 | } 189 | } 190 | 191 | 调用 `select` 方法进行连接。 192 | 193 | int nSelect = select(have_fds ? hSocketMax + 1 : 0,&fdsetRecv, &fdsetSend, &fdsetError, &timeout); 194 | 195 | 接下来,检查 `select` 调用是否出错。 196 | 197 | if (nSelect == SOCKET_ERROR) 198 | { 199 | if (have_fds) 200 | { 201 | int nErr = WSAGetLastError(); 202 | LogPrintf("socket select error %s\n", NetworkErrorString(nErr)); 203 | for (unsigned int i = 0; i <= hSocketMax; i++) 204 | FD_SET(i, &fdsetRecv); 205 | } 206 | FD_ZERO(&fdsetSend); 207 | FD_ZERO(&fdsetError); 208 | if (!interruptNet.sleep_for(std::chrono::milliseconds(timeout.tv_usec/1000))) 209 | return; 210 | } 211 | 212 | 6. 接下来,接收新的连接。 213 | 214 | 遍历所有监听的套接字,如果当前套接字有效,并且套接字在在接收数据的集合中,即新的连接请求进来,则调用 `AcceptConnection` 方法进行处理。 215 | 216 | for (const ListenSocket& hListenSocket : vhListenSocket) 217 | { 218 | if (hListenSocket.socket != INVALID_SOCKET && FD_ISSET(hListenSocket.socket, &fdsetRecv)) 219 | { 220 | AcceptConnection(hListenSocket); 221 | } 222 | } 223 | 224 | `AcceptConnection` 方法处理远程对等节点的连接逻辑,具体如下: 225 | 226 | - 调用 `accept` 方法,接受客户端连接。 227 | 228 | SOCKET hSocket = accept(hListenSocket.socket, (struct sockaddr*)&sockaddr, &len); 229 | 230 | - 计算最大的入站连接数 231 | 232 | int nMaxInbound = nMaxConnections - (nMaxOutbound + nMaxFeeler); 233 | 234 | - 如果连接成功,那么调用地址对象的 `SetSockAddr` 方法来保存保存对等节点的地址。 235 | 236 | if (hSocket != INVALID_SOCKET) { 237 | if (!addr.SetSockAddr((const struct sockaddr*)&sockaddr)) { 238 | LogPrintf("Warning: Unknown socket family\n"); 239 | } 240 | } 241 | 242 | - 遍历对等节点列表,如果当前对等节点属于入站类型,则变量 nInbound 加 1。 243 | 244 | { 245 | LOCK(cs_vNodes); 246 | for (const CNode* pnode : vNodes) { 247 | if (pnode->fInbound) nInbound++; 248 | } 249 | } 250 | 251 | - 如果连接不成功,则直接退出。 252 | 253 | if (hSocket == INVALID_SOCKET) 254 | { 255 | int nErr = WSAGetLastError(); 256 | if (nErr != WSAEWOULDBLOCK) 257 | LogPrintf("socket error accept failed: %s\n", NetworkErrorString(nErr)); 258 | return; 259 | } 260 | 261 | - 如果网络是不活跃的,则关闭套接字并返回。 262 | 263 | if (!fNetworkActive) { 264 | LogPrintf("connection from %s dropped: not accepting new connections\n", addr.ToString()); 265 | CloseSocket(hSocket); 266 | return; 267 | } 268 | 269 | - 如果是不可连接的,则关闭套接字并返回。 270 | 271 | if (!IsSelectableSocket(hSocket)) 272 | { 273 | LogPrintf("connection from %s dropped: non-selectable socket\n", addr.ToString()); 274 | CloseSocket(hSocket); 275 | return; 276 | } 277 | 278 | `IsSelectableSocket` 方法是一个内联方法,如果 Win32 则直接返回真,否则如果 `select` 函数返回值小于 `FD_SETSIZE` 则返回真,否则返回假。 279 | 280 | - 如果入站数量已经达到最大的入站数量,则调用 `AttemptToEvictConnection` 方法,找到要退出的连接。如果找不到则关闭套接字并返回。 281 | 282 | if (nInbound >= nMaxInbound) 283 | { 284 | if (!AttemptToEvictConnection()) { 285 | LogPrint(BCLog::NET, "failed to find an eviction candidate - connection dropped (full)\n"); 286 | CloseSocket(hSocket); 287 | return; 288 | } 289 | } 290 | 291 | - 生成节点对象,并进行相关设置,然后加入节点集合中 `vNodes`。 292 | 293 | NodeId id = GetNewNodeId(); 294 | uint64_t nonce = GetDeterministicRandomizer(RANDOMIZER_ID_LOCALHOSTNONCE).Write(id).Finalize(); 295 | CAddress addr_bind = GetBindAddress(hSocket); 296 | 297 | CNode* pnode = new CNode(id, nLocalServices, GetBestHeight(), hSocket, addr, CalculateKeyedNetGroup(addr), nonce, addr_bind, "", true); 298 | pnode->AddRef(); 299 | pnode->fWhitelisted = whitelisted; 300 | m_msgproc->InitializeNode(pnode); 301 | 302 | LogPrint(BCLog::NET, "connection from %s accepted\n", addr.ToString()); 303 | 304 | { 305 | LOCK(cs_vNodes); 306 | vNodes.push_back(pnode); 307 | } 308 | 309 | 7. 处理节点的引用数量 310 | 311 | std::vector vNodesCopy; 312 | { 313 | LOCK(cs_vNodes); 314 | vNodesCopy = vNodes; 315 | for (CNode* pnode : vNodesCopy) 316 | pnode->AddRef(); 317 | } 318 | 319 | `AddRef` 方法会把 `nRefCount` 加1。 320 | 321 | 8. 遍历所有的节点进行收发信息处理。 322 | 323 | - 判断当前节点是否在读写、错误集合中。 324 | 325 | bool recvSet = false; 326 | bool sendSet = false; 327 | bool errorSet = false; 328 | { 329 | LOCK(pnode->cs_hSocket); 330 | if (pnode->hSocket == INVALID_SOCKET) 331 | continue; 332 | recvSet = FD_ISSET(pnode->hSocket, &fdsetRecv); 333 | sendSet = FD_ISSET(pnode->hSocket, &fdsetSend); 334 | errorSet = FD_ISSET(pnode->hSocket, &fdsetError); 335 | } 336 | 337 | - 如果当前节点在读取集合或错误集合中,则进行下面的处理: 338 | 339 | 调用 `recv` 方法,读取数据。 340 | 341 | char pchBuf[0x10000]; 342 | int nBytes = 0; 343 | { 344 | LOCK(pnode->cs_hSocket); 345 | if (pnode->hSocket == INVALID_SOCKET) 346 | continue; 347 | nBytes = recv(pnode->hSocket, pchBuf, sizeof(pchBuf), MSG_DONTWAIT); 348 | } 349 | 350 | 如果读取的数量大于0,则:调用 `ReceiveMsgBytes` 方法,从缓冲区中读取给定数量的数据,并生成 `CNetMessage` 对象。如果出错,则调用 `CloseSocketDisconnect` 方法,关闭套接字并断开连接。 351 | 352 | 如果读取的数量等于0,即远程节点已经关闭,则:调用 `CloseSocketDisconnect` 方法,关闭套接字并断开连接。 353 | 354 | 如果读取的数量小于0,即读取过程中出错,则:调用 `CloseSocketDisconnect` 方法,关闭套接字并断开连接。 355 | 356 | 具体代码如下: 357 | 358 | if (recvSet || errorSet) 359 | { 360 | // typical socket buffer is 8K-64K 361 | char pchBuf[0x10000]; 362 | int nBytes = 0; 363 | { 364 | LOCK(pnode->cs_hSocket); 365 | if (pnode->hSocket == INVALID_SOCKET) 366 | continue; 367 | nBytes = recv(pnode->hSocket, pchBuf, sizeof(pchBuf), MSG_DONTWAIT); 368 | } 369 | if (nBytes > 0) 370 | { 371 | bool notify = false; 372 | if (!pnode->ReceiveMsgBytes(pchBuf, nBytes, notify)) 373 | pnode->CloseSocketDisconnect(); 374 | RecordBytesRecv(nBytes); 375 | if (notify) { 376 | size_t nSizeAdded = 0; 377 | auto it(pnode->vRecvMsg.begin()); 378 | for (; it != pnode->vRecvMsg.end(); ++it) { 379 | if (!it->complete()) 380 | break; 381 | nSizeAdded += it->vRecv.size() + CMessageHeader::HEADER_SIZE; 382 | } 383 | { 384 | LOCK(pnode->cs_vProcessMsg); 385 | pnode->vProcessMsg.splice(pnode->vProcessMsg.end(), pnode->vRecvMsg, pnode->vRecvMsg.begin(), it); 386 | pnode->nProcessQueueSize += nSizeAdded; 387 | pnode->fPauseRecv = pnode->nProcessQueueSize > nReceiveFloodSize; 388 | } 389 | WakeMessageHandler(); 390 | } 391 | } 392 | else if (nBytes == 0) 393 | { 394 | // socket closed gracefully 395 | if (!pnode->fDisconnect) { 396 | LogPrint(BCLog::NET, "socket closed\n"); 397 | } 398 | pnode->CloseSocketDisconnect(); 399 | } 400 | else if (nBytes < 0) 401 | { 402 | // error 403 | int nErr = WSAGetLastError(); 404 | if (nErr != WSAEWOULDBLOCK && nErr != WSAEMSGSIZE && nErr != WSAEINTR && nErr != WSAEINPROGRESS) 405 | { 406 | if (!pnode->fDisconnect) 407 | LogPrintf("socket recv error %s\n", NetworkErrorString(nErr)); 408 | pnode->CloseSocketDisconnect(); 409 | } 410 | } 411 | } 412 | 413 | - 如果当前节点在发送集合中,则进行如下处理。 414 | 415 | if (sendSet) 416 | { 417 | LOCK(pnode->cs_vSend); 418 | size_t nBytes = SocketSendData(pnode); 419 | if (nBytes) { 420 | RecordBytesSent(nBytes); 421 | } 422 | } 423 | 424 | `SocketSendData` 方法主要逻辑是遍历节点的发送消息集合 `vSendMsg`,然后调用 `send` 方法发送每一个消息,并针对发送正确与否进行处理;同时从发送消息集合 `vSendMsg` 对应的消息。 425 | 426 | size_t CConnman::SocketSendData(CNode *pnode) const 427 | { 428 | auto it = pnode->vSendMsg.begin(); 429 | size_t nSentSize = 0; 430 | 431 | while (it != pnode->vSendMsg.end()) { 432 | const auto &data = *it; 433 | assert(data.size() > pnode->nSendOffset); 434 | int nBytes = 0; 435 | { 436 | LOCK(pnode->cs_hSocket); 437 | if (pnode->hSocket == INVALID_SOCKET) 438 | break; 439 | nBytes = send(pnode->hSocket, reinterpret_cast(data.data()) + pnode->nSendOffset, data.size() - pnode->nSendOffset, MSG_NOSIGNAL | MSG_DONTWAIT); 440 | } 441 | if (nBytes > 0) { 442 | pnode->nLastSend = GetSystemTimeInSeconds(); 443 | pnode->nSendBytes += nBytes; 444 | pnode->nSendOffset += nBytes; 445 | nSentSize += nBytes; 446 | if (pnode->nSendOffset == data.size()) { 447 | pnode->nSendOffset = 0; 448 | pnode->nSendSize -= data.size(); 449 | pnode->fPauseSend = pnode->nSendSize > nSendBufferMaxSize; 450 | it++; 451 | } else { 452 | // could not send full message; stop sending more 453 | break; 454 | } 455 | } else { 456 | if (nBytes < 0) { 457 | // error 458 | int nErr = WSAGetLastError(); 459 | if (nErr != WSAEWOULDBLOCK && nErr != WSAEMSGSIZE && nErr != WSAEINTR && nErr != WSAEINPROGRESS) 460 | { 461 | LogPrintf("socket send error %s\n", NetworkErrorString(nErr)); 462 | pnode->CloseSocketDisconnect(); 463 | } 464 | } 465 | // couldn't send anything at all 466 | break; 467 | } 468 | } 469 | 470 | if (it == pnode->vSendMsg.end()) { 471 | assert(pnode->nSendOffset == 0); 472 | assert(pnode->nSendSize == 0); 473 | } 474 | pnode->vSendMsg.erase(pnode->vSendMsg.begin(), it); 475 | return nSentSize; 476 | } 477 | 478 | 9. 接下来对节点的活跃性进行检查。 479 | 480 | int64_t nTime = GetSystemTimeInSeconds(); 481 | if (nTime - pnode->nTimeConnected > 60) 482 | { 483 | if (pnode->nLastRecv == 0 || pnode->nLastSend == 0) 484 | { 485 | LogPrint(BCLog::NET, "socket no message in first 60 seconds, %d %d from %d\n", pnode->nLastRecv != 0, pnode->nLastSend != 0, pnode->GetId()); 486 | pnode->fDisconnect = true; 487 | } 488 | else if (nTime - pnode->nLastSend > TIMEOUT_INTERVAL) 489 | { 490 | LogPrintf("socket sending timeout: %is\n", nTime - pnode->nLastSend); 491 | pnode->fDisconnect = true; 492 | } 493 | else if (nTime - pnode->nLastRecv > (pnode->nVersion > BIP0031_VERSION ? TIMEOUT_INTERVAL : 90*60)) 494 | { 495 | LogPrintf("socket receive timeout: %is\n", nTime - pnode->nLastRecv); 496 | pnode->fDisconnect = true; 497 | } 498 | else if (pnode->nPingNonceSent && pnode->nPingUsecStart + TIMEOUT_INTERVAL * 1000000 < GetTimeMicros()) 499 | { 500 | LogPrintf("ping timeout: %fs\n", 0.000001 * (GetTimeMicros() - pnode->nPingUsecStart)); 501 | pnode->fDisconnect = true; 502 | } 503 | else if (!pnode->fSuccessfullyConnected) 504 | { 505 | LogPrint(BCLog::NET, "version handshake timeout from %d\n", pnode->GetId()); 506 | pnode->fDisconnect = true; 507 | } 508 | } 509 | } 510 | 511 | 10. 遍历所有节点,减少引用数。 512 | 513 | { 514 | LOCK(cs_vNodes); 515 | for (CNode* pnode : vNodesCopy) 516 | pnode->Release(); 517 | } 518 | 519 | `Release` 方法把 `nRefCount` 参数减1。 520 | 521 | -------------------------------------------------------------------------------- /start/setup01.md: -------------------------------------------------------------------------------- 1 | # 如何接入比特币网络以及原理分析 2 | 3 | 以下内容为系统启动过程中,每一步骤的详细分析。 4 | 5 | 6 | ## 第1步,应用初始化基本设置(`src/bitcoind.cpp`) 7 | 8 | `AppInitBasicSetup` 函数进行基本的设置。 9 | 10 | 1. 调用 `SetupNetworking` 函数,进行网络设置。 11 | 12 | 主要是针对 Win32 系统处理套接字,别的系统直接返回真。 13 | 14 | 2. 如果不是 WIN32 系统,进行下面的处理: 15 | 16 | - 如果设置 `sysperms` 参数为真,调用 `umask` 函数,设置位码为 077。 17 | 18 | - 调用 `registerSignalHandler` 函数,设置 `SIGTERM` 信号处理器为 `HandleSIGTERM`;`SIGINT` 为 `HandleSIGTERM`;`SIGHUP` 为 `HandleSIGHUP`。 19 | -------------------------------------------------------------------------------- /start/setup02.md: -------------------------------------------------------------------------------- 1 | # 如何接入比特币网络以及原理分析 2 | 3 | 以下内容为系统启动过程中,每一步骤的详细分析。 4 | 5 | 6 | ## 第2步,应用初始参数交互设置(`src/bitcoind.cpp`) 7 | 8 | `AppInitParameterInteraction` 函数前半部分。 9 | 10 | 1. 首先,调用 `Params` 方法,获取前面初始化的 `globalChainParams` 区块链对象。 11 | 12 | const CChainParams& chainparams = Params(); 13 | 14 | 根据不同的网络,chainparams 的真实类型可能是 `CMainParams`,代表主网络;或者是 `CTestNetParams`,代表测试网络;或者是 `CRegTestParams` 代表回归测试网络。 15 | 16 | 2. 检查指定的区块目录是否存。如果不存在,则返回初始化错误。 17 | 18 | if (!fs::is_directory(GetBlocksDir(false))) { 19 | return InitError(strprintf(_("Specified blocks directory \"%s\" does not exist."), gArgs.GetArg("-blocksdir", "").c_str())); 20 | } 21 | 22 | 3. 如果同时指定了 `prune`、`txindex`,则抛出初始化错误。 23 | 24 | 如果指定了区块修剪 `prune`,就要禁止交易索引 `txindex`,两者不兼容,只能其一。 25 | 26 | if (gArgs.GetArg("-prune", 0)) { 27 | if (gArgs.GetBoolArg("-txindex", DEFAULT_TXINDEX)) 28 | return InitError(_("Prune mode is incompatible with -txindex.")); 29 | } 30 | 31 | 4. 检查是否指定了 `-bind` 或 `-whitebind` 两者之一,并且同时禁止其他节点连接(`listen`)。如果是,则抛出初始化错误。 32 | 33 | size_t nUserBind = gArgs.GetArgs("-bind").size() + gArgs.GetArgs("-whitebind").size(); 34 | if (nUserBind != 0 && !gArgs.GetBoolArg("-listen", DEFAULT_LISTEN)) { 35 | return InitError("Cannot set -bind or -whitebind together with -listen=0"); 36 | } 37 | 38 | 5. 确保有足够的文件符可用。 39 | 40 | 因为在类 Unix 系统中,每个套接字都是一个文件,都需要一个文件描述符。所以要检查指定的最大连接数 `maxconnections` 是否超过系统可用限制。 41 | 42 | int nBind = std::max(nUserBind, size_t(1)); 43 | nUserMaxConnections = gArgs.GetArg("-maxconnections", DEFAULT_MAX_PEER_CONNECTIONS); 44 | nMaxConnections = std::max(nUserMaxConnections, 0); 45 | 46 | nMaxConnections = std::max(std::min(nMaxConnections, FD_SETSIZE - nBind - MIN_CORE_FILEDESCRIPTORS - MAX_ADDNODE_CONNECTIONS), 0); 47 | nFD = RaiseFileDescriptorLimit(nMaxConnections + MIN_CORE_FILEDESCRIPTORS + MAX_ADDNODE_CONNECTIONS); 48 | if (nFD < MIN_CORE_FILEDESCRIPTORS) 49 | return InitError(_("Not enough file descriptors available.")); 50 | nMaxConnections = std::min(nFD - MIN_CORE_FILEDESCRIPTORS - MAX_ADDNODE_CONNECTIONS, nMaxConnections); 51 | 52 | if (nMaxConnections < nUserMaxConnections) 53 | InitWarning(strprintf(_("Reducing -maxconnections from %d to %d, because of system limitations."), nUserMaxConnections, nMaxConnections)); 54 | 55 | -------------------------------------------------------------------------------- /start/setup03.md: -------------------------------------------------------------------------------- 1 | # 如何接入比特币网络以及原理分析 2 | 3 | 以下内容为系统启动过程中,每一步骤的详细分析。 4 | 5 | 6 | ## 第3步,参数到内部标志的转换处理(`src/bitcoind.cpp`) 7 | 8 | `AppInitParameterInteraction` 函数后半部分。 9 | 10 | 1. 处理 `debug`、`debugexclude`、`debugnet` 等参数。 11 | 12 | 如果指定了 `-debug`,则解析每个类别是否是支持的类别。如果不支持,则输入警告消息。如果需要同时指定多个类别,可分开指定,比如要调试网络与RPC 相关的信息,则配置如下:`-debug=net -debug=rpc`。 13 | 14 | if (gArgs.IsArgSet("-debug")) { 15 | const std::vector categories = gArgs.GetArgs("-debug"); 16 | 17 | if (std::none_of(categories.begin(), categories.end(), 18 | [](std::string cat){return cat == "0" || cat == "none";})) { 19 | for (const auto& cat : categories) { 20 | if (!g_logger->EnableCategory(cat)) { 21 | InitWarning(strprintf(_("Unsupported logging category %s=%s."), "-debug", cat)); 22 | } 23 | } 24 | } 25 | } 26 | 27 | 2. 如果指定了 `-socks` 参数,则提示使用 SOCKS5 28 | 29 | if (gArgs.IsArgSet("-socks")) 30 | return InitError(_("Unsupported argument -socks found. Setting SOCKS version isn't possible anymore, only SOCKS5 proxies are supported.")); 31 | 32 | 3. 如果指定了 `-tor` 参数,则提示使用 `-onion`。 33 | 34 | if (gArgs.GetBoolArg("-tor", false)) 35 | return InitError(_("Unsupported argument -tor found, use -onion.")); 36 | 37 | 4. 如果指定了 `-benchmark` 参数,则提示使用 `-debug=bench`。 38 | 39 | if (gArgs.GetBoolArg("-benchmark", false)) 40 | InitWarning(_("Unsupported argument -benchmark ignored, use -debug=bench.")); 41 | 42 | 5. 如果指定了 `-whitelistalwaysrelay` 参数,则提示使用 `-whitelistrelay`,或`-whitelistforcerelay`。 43 | 44 | if (gArgs.GetBoolArg("-whitelistalwaysrelay", false)) 45 | InitWarning(_("Unsupported argument -whitelistalwaysrelay ignored, use -whitelistrelay and/or -whitelistforcerelay.")); 46 | 47 | 6. 如果指定了 `-blockminsize` 参数,则提示使用 `-blockminsize`。 48 | 49 | if (gArgs.IsArgSet("-blockminsize")) 50 | InitWarning("Unsupported argument -blockminsize ignored."); 51 | 52 | 7. 根据是否指定 `-checkmempool` 参数,确定是否进行合理性检查。 53 | 54 | 在不指定这个参数的情况下,当运行主网络和测试网络时,不进行交易池合理性检查,当运行回归测试网络时,进行合理性检查。代码如下: 55 | 56 | // Checkmempool and checkblockindex default to true in regtest mode 57 | 58 | // 当运行主网络和测试网络时,DefaultConsistencyChecks 函数返回假,导致变量 ratio 为1, 为0,从而不进行交易池设置;当运行回归测试网络时,函数返回真,从而变量 ratio 为1,从而进行交易池设置 59 | int ratio = std::min(std::max(gArgs.GetArg("-checkmempool", chainparams.DefaultConsistencyChecks() ? 1 : 0), 0), 1000000); 60 | if (ratio != 0) { 61 | mempool.setSanityCheck(1.0 / ratio); 62 | } 63 | 64 | fCheckBlockIndex = gArgs.GetBoolArg("-checkblockindex", chainparams.DefaultConsistencyChecks()); 65 | 66 | 8. 设置检查点默认打开 67 | 68 | fCheckpointsEnabled = gArgs.GetBoolArg("-checkpoints", DEFAULT_CHECKPOINTS_ENABLED); 69 | 70 | 9. 处理 `assumevalid` 参数。 71 | 72 | 这个参数的意思是假设有效,即如果如果这个块在链中,则假定它和它的祖先是有效的,并且可能跳过它们的脚本验证。否则会验证所有的块。 73 | 74 | hashAssumeValid = uint256S(gArgs.GetArg("-assumevalid", chainparams.GetConsensus().defaultAssumeValid.GetHex())); 75 | if (!hashAssumeValid.IsNull()) 76 | LogPrintf("Assuming ancestors of block %s have valid signatures.\n", hashAssumeValid.GetHex()); 77 | else 78 | LogPrintf("Validating signatures for all blocks.\n"); 79 | 80 | 10. 根据是否指定 `minimumchainwork` 参数,确定使用默认的最小工作量还是使用用户指定的最小工作量。 81 | 82 | if (gArgs.IsArgSet("-minimumchainwork")) { 83 | const std::string minChainWorkStr = gArgs.GetArg("-minimumchainwork", ""); 84 | if (!IsHexNumber(minChainWorkStr)) { 85 | return InitError(strprintf("Invalid non-hex (%s) minimum chain work value specified", minChainWorkStr)); 86 | } 87 | nMinimumChainWork = UintToArith256(uint256S(minChainWorkStr)); 88 | } else { 89 | nMinimumChainWork = UintToArith256(chainparams.GetConsensus().nMinimumChainWork); 90 | } 91 | 92 | 11. 计算内存池/交易池限制,包括处理 `maxmempool`、`limitdescendantsize` 参数。 93 | 94 | 前者表示最大内存池,后者表示最小内存池,如果最大值小于最小值,则抛出初始化错误。 95 | 96 | int64_t nMempoolSizeMax = gArgs.GetArg("-maxmempool", DEFAULT_MAX_MEMPOOL_SIZE) * 1000000; 97 | int64_t nMempoolSizeMin = gArgs.GetArg("-limitdescendantsize", DEFAULT_DESCENDANT_SIZE_LIMIT) * 1000 * 40; 98 | if (nMempoolSizeMax < 0 || nMempoolSizeMax < nMempoolSizeMin) 99 | return InitError(strprintf(_("-maxmempool must be at least %d MB"), std::ceil(nMempoolSizeMin / 1000000.0))); 100 | 101 | 12. 如果指定了 `incrementalrelayfee`,则进行相关处理。 102 | 103 | `incrementalrelayfee` 定义中继的成本费率,应用于交易池限制和 BIP 125 替换。代码如下: 104 | 105 | if (gArgs.IsArgSet("-incrementalrelayfee")) 106 | { 107 | CAmount n = 0; 108 | if (!ParseMoney(gArgs.GetArg("-incrementalrelayfee", ""), n)) 109 | return InitError(AmountErrMsg("incrementalrelayfee", gArgs.GetArg("-incrementalrelayfee", ""))); 110 | incrementalRelayFee = CFeeRate(n); 111 | } 112 | 113 | 13. 处理 `-par` 参数,指定脚本签名的线程数量。 114 | 115 | 代码如下: 116 | 117 | if (nScriptCheckThreads <= 0) 118 | nScriptCheckThreads += GetNumCores(); 119 | if (nScriptCheckThreads <= 1) 120 | nScriptCheckThreads = 0; 121 | else if (nScriptCheckThreads > MAX_SCRIPTCHECK_THREADS) 122 | nScriptCheckThreads = MAX_SCRIPTCHECK_THREADS; 123 | 124 | 14. 处理区块修剪参数 `-prune`。 125 | 126 | 代码如下: 127 | 128 | int64_t nPruneArg = gArgs.GetArg("-prune", 0); 129 | if (nPruneArg < 0) { 130 | return InitError(_("Prune cannot be configured with a negative value.")); 131 | 132 | nPruneTarget = (uint64_t) nPruneArg * 1024 * 1024; 133 | if (nPruneArg == 1) { // manual pruning: -prune=1 134 | LogPrintf("Block pruning enabled. Use RPC call pruneblockchain(height) to manually prune block and undo files.\n"); 135 | nPruneTarget = std::numeric_limits::max(); 136 | fPruneMode = true; 137 | } else if (nPruneTarget) { 138 | if (nPruneTarget < MIN_DISK_SPACE_FOR_BLOCK_FILES) { 139 | return InitError(strprintf(_("Prune configured below the minimum of %d MiB. Please use a higher number."), MIN_DISK_SPACE_FOR_BLOCK_FILES / 1024 / 1024)); 140 | } 141 | LogPrintf("Prune configured to target %uMiB on disk for block and undo files.\n", nPruneTarget / 1024 / 1024); 142 | fPruneMode = true; 143 | } 144 | 145 | 15. 处理连接超时时间 `-timeout`。 146 | 147 | 代码如下: 148 | 149 | nConnectTimeout = gArgs.GetArg("-timeout", DEFAULT_CONNECT_TIMEOUT); 150 | if (nConnectTimeout <= 0) 151 | nConnectTimeout = DEFAULT_CONNECT_TIMEOUT; 152 | 153 | 16. 处理 `-minrelaytxfee` 参数。 154 | 155 | 对于中继、挖矿和交易创建,小于此的费用被认为是零费用。解析并计算最小中继交易费用。代码如下: 156 | 157 | if (gArgs.IsArgSet("-minrelaytxfee")) { 158 | CAmount n = 0; 159 | if (!ParseMoney(gArgs.GetArg("-minrelaytxfee", ""), n)) { 160 | return InitError(AmountErrMsg("minrelaytxfee", gArgs.GetArg("-minrelaytxfee", ""))); 161 | } 162 | ::minRelayTxFee = CFeeRate(n); 163 | } else if (incrementalRelayFee > ::minRelayTxFee) { 164 | ::minRelayTxFee = incrementalRelayFee; 165 | LogPrintf("Increasing minrelaytxfee to %s to match incrementalrelayfee\n",::minRelayTxFee.ToString()); 166 | } 167 | 168 | 17. 处理 `-blockmintxfee` 参数。 169 | 170 | 设置要在块创建中包含的事务满足的最低费率,即低于这个费率,交易将不进行打包。代码如下: 171 | 172 | if (gArgs.IsArgSet("-blockmintxfee")) 173 | { 174 | CAmount n = 0; 175 | if (!ParseMoney(gArgs.GetArg("-blockmintxfee", ""), n)) 176 | return InitError(AmountErrMsg("blockmintxfee", gArgs.GetArg("-blockmintxfee", ""))); 177 | } 178 | 179 | 18. 处理 `-dustrelayfee` 参数。 180 | 181 | 具体代码如下: 182 | 183 | if (gArgs.IsArgSet("-dustrelayfee")) 184 | { 185 | CAmount n = 0; 186 | if (!ParseMoney(gArgs.GetArg("-dustrelayfee", ""), n)) 187 | return InitError(AmountErrMsg("dustrelayfee", gArgs.GetArg("-dustrelayfee", ""))); 188 | dustRelayFee = CFeeRate(n); 189 | } 190 | 191 | 19. 处理 `-acceptnonstdtxn` 参数。 192 | 193 | 这个参数代表中继和挖掘非标准交易。具体代码如下: 194 | 195 | fRequireStandard = !gArgs.GetBoolArg("-acceptnonstdtxn", !chainparams.RequireStandard()); 196 | if (chainparams.RequireStandard() && !fRequireStandard) 197 | return InitError(strprintf("acceptnonstdtxn is not currently supported for %s chain", chainparams.NetworkIDString())); 198 | 199 | 20. 处理 `-bytespersigop` 参数。 200 | 201 | 计算中继和挖掘中交易的 sigop 的等效字节数。 202 | 203 | nBytesPerSigOp = gArgs.GetArg("-bytespersigop", nBytesPerSigOp); 204 | 205 | 21. 调用钱包初始接口对象的 `ParameterInteraction` 方法,初始钱包相关的参数。本方法在 `wallet/init.cpp` 文件中。 206 | 207 | 调用代码如下: 208 | 209 | if (!g_wallet_init_interface.ParameterInteraction()) return false; 210 | 211 | 代码内部具体处理如下: 212 | 213 | - 检查是否禁用钱包 `-disablewallet`。如果禁用,就不会加载钱包,并且会禁用钱包 RPC,这种情况下忽略 `-wallet`。 214 | 215 | if (gArgs.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET)) { 216 | for (const std::string& wallet : gArgs.GetArgs("-wallet")) { 217 | LogPrintf("%s: parameter interaction: -disablewallet -> ignoring -wallet=%s\n", __func__, wallet); 218 | } 219 | 220 | return true; 221 | } 222 | 223 | - 设置 `-wallet` 参数的默认值为空。确定指定的是单一钱包还是多钱包。 224 | 225 | gArgs.SoftSetArg("-wallet", ""); 226 | const bool is_multiwallet = gArgs.GetArgs("-wallet").size() > 1; 227 | 228 | - 处理 `-blocksonly`、`-walletbroadcast` 参数。 229 | 230 | - 处理 `-salvagewallet`、`-rescan` 参数。 231 | 232 | 如果指定了在启动时试图从损坏的钱包中恢复私钥,即 `-salvagewallet` 参数,那么不能使用多个钱包。 233 | 234 | if (gArgs.GetBoolArg("-salvagewallet", false)) { 235 | if (is_multiwallet) { 236 | return InitError(strprintf("%s is only allowed with a single wallet file", "-salvagewallet")); 237 | } 238 | 239 | if (gArgs.SoftSetBoolArg("-rescan", true)) { 240 | LogPrintf("%s: parameter interaction: -salvagewallet=1 -> setting -rescan=1\n", __func__); 241 | } 242 | } 243 | 244 | - 处理 `-zapwallettxes`、`-persistmempool` 参数。 245 | 246 | `-zapwallettxes` 参数表示删除所有钱包交易,并且仅在启动时通过 `-rescan` 恢复区块链相关的那些部分(1 =保留tx元数据,例如账户所有者和支付请求信息,2 =丢弃tx元数据)。它暗示了在启动时删除交易池中的那些交易。同时,它也暗示了进行区块链扫描,即不能是多钱包。 247 | 248 | bool zapwallettxes = gArgs.GetBoolArg("-zapwallettxes", false); 249 | if (zapwallettxes && gArgs.SoftSetBoolArg("-persistmempool", false)) { 250 | LogPrintf("%s: parameter interaction: -zapwallettxes enabled -> setting -persistmempool=0\n", __func__); 251 | } 252 | 253 | if (zapwallettxes) { 254 | if (is_multiwallet) { 255 | return InitError(strprintf("%s is only allowed with a single wallet file", "-zapwallettxes")); 256 | } 257 | if (gArgs.SoftSetBoolArg("-rescan", true)) { 258 | LogPrintf("%s: parameter interaction: -zapwallettxes enabled -> setting -rescan=1\n", __func__); 259 | } 260 | } 261 | 262 | - 检查是否指定了 `-upgradewallet` 参数。 263 | 264 | 在多钱包情况下,不能进行钱包升级。 265 | 266 | if (is_multiwallet) { 267 | if (gArgs.GetBoolArg("-upgradewallet", false)) { 268 | return InitError(strprintf("%s is only allowed with a single wallet file", "-upgradewallet")); 269 | } 270 | } 271 | 272 | - 检查是否指定了 `-sysperms` 参数。 273 | 274 | 如果指定了 `-sysperms` 参数,则抛出初始异常错误。这个参数表示了用系统默认的权限创建一个新文件,而不是 `077`,只有在禁用钱包功能的情况下才有效。 275 | 276 | if (gArgs.GetBoolArg("-sysperms", false)) 277 | return InitError("-sysperms is not allowed in combination with enabled wallet functionality"); 278 | 279 | - 检查是否同时指定了 `-prune`、`-rescan` 参数。 280 | 281 | 在指定了修剪模式的情况下,不能执行扫描区块链的动作。所以如果同时指定了这两个参数,抛出错误。 282 | 283 | if (gArgs.GetArg("-prune", 0) && gArgs.GetBoolArg("-rescan", false)) 284 | return InitError(_("Rescans are not possible in pruned mode. You will need to use -reindex which will download the whole blockchain again.")); 285 | 286 | - 处理 `-maxtxfee` 参数。 287 | 288 | `-maxtxfee` 参数表示了在单个钱包的交易或原始交易中使用的最高总费用。如果设置过小,可能会中止大型交易。这个值不能小于最小中继交易费用。 289 | 290 | if (gArgs.IsArgSet("-maxtxfee")) 291 | { 292 | CAmount nMaxFee = 0; 293 | if (!ParseMoney(gArgs.GetArg("-maxtxfee", ""), nMaxFee)) 294 | return InitError(AmountErrMsg("maxtxfee", gArgs.GetArg("-maxtxfee", ""))); 295 | if (nMaxFee > HIGH_MAX_TX_FEE) 296 | InitWarning(_("-maxtxfee is set very high! Fees this large could be paid on a single transaction.")); 297 | maxTxFee = nMaxFee; 298 | if (CFeeRate(maxTxFee, 1000) < ::minRelayTxFee) 299 | { 300 | return InitError(strprintf(_("Invalid amount for -maxtxfee=: '%s' (must be at least the minrelay fee of %s to prevent stuck transactions)"), 301 | gArgs.GetArg("-maxtxfee", ""), ::minRelayTxFee.ToString())); 302 | } 303 | } 304 | 305 | 22. 获取 `-permitbaremultisig`、`-datacarrier`、`-datacarriersize`等参数。 306 | 307 | fIsBareMultisigStd = gArgs.GetBoolArg("-permitbaremultisig", DEFAULT_PERMIT_BAREMULTISIG); 308 | fAcceptDatacarrier = gArgs.GetBoolArg("-datacarrier", DEFAULT_ACCEPT_DATACARRIER); 309 | nMaxDatacarrierBytes = gArgs.GetArg("-datacarriersize", nMaxDatacarrierBytes); 310 | 311 | 23. 调用 `-SetMockTime` 方法,设置模拟时间。 312 | 313 | SetMockTime(gArgs.GetArg("-mocktime", 0)); 314 | 315 | 24. 根据 `-peerbloomfilters` 参数,设置本地支持的服务。 316 | 317 | if (gArgs.GetBoolArg("-peerbloomfilters", DEFAULT_PEERBLOOMFILTERS)) 318 | nLocalServices = ServiceFlags(nLocalServices | NODE_BLOOM); 319 | 320 | 25. 检测 `-rpcserialversion` 参数是否小于0,是否大于1。 321 | 322 | if (gArgs.GetArg("-rpcserialversion", DEFAULT_RPC_SERIALIZE_VERSION) < 0) 323 | return InitError("rpcserialversion must be non-negative."); 324 | 325 | if (gArgs.GetArg("-rpcserialversion", DEFAULT_RPC_SERIALIZE_VERSION) > 1) 326 | return InitError("unknown rpcserialversion requested."); 327 | 328 | 26. 获取 `-maxtipage` 参数值。 329 | 330 | nMaxTipAge = gArgs.GetArg("-maxtipage", DEFAULT_MAX_TIP_AGE); 331 | 332 | 27. 处理 `-mempoolreplacement` 参数。 333 | 334 | `-mempoolreplacement` 参数表示是否启用交易池交易替换。 335 | 336 | fEnableReplacement = gArgs.GetBoolArg("-mempoolreplacement", DEFAULT_ENABLE_REPLACEMENT); 337 | if ((!fEnableReplacement) && gArgs.IsArgSet("-mempoolreplacement")) { 338 | std::string strReplacementModeList = gArgs.GetArg("-mempoolreplacement", ""); // default is impossible 339 | std::vector vstrReplacementModes; 340 | boost::split(vstrReplacementModes, strReplacementModeList, boost::is_any_of(",")); 341 | fEnableReplacement = (std::find(vstrReplacementModes.begin(), vstrReplacementModes.end(), "fee") != vstrReplacementModes.end()); 342 | } 343 | 344 | 28. 处理 `-vbparams` 参数。 345 | 346 | `-vbparams` 参数代表了对于指定的版本位部署,使用给定的开始/结束时间。 347 | 348 | 重载版本位只在回归测试模式下才允许,否则会抛出初始异常错误。在回归测试模式下检查所有指定的版本位。 349 | 350 | if (gArgs.IsArgSet("-vbparams")) { 351 | // Allow overriding version bits parameters for testing 352 | if (!chainparams.MineBlocksOnDemand()) { 353 | return InitError("Version bits parameters may only be overridden on regtest."); 354 | } 355 | for (const std::string& strDeployment : gArgs.GetArgs("-vbparams")) { 356 | std::vector vDeploymentParams; 357 | boost::split(vDeploymentParams, strDeployment, boost::is_any_of(":")); 358 | if (vDeploymentParams.size() != 3) { 359 | return InitError("Version bits parameters malformed, expecting deployment:start:end"); 360 | } 361 | int64_t nStartTime, nTimeout; 362 | if (!ParseInt64(vDeploymentParams[1], &nStartTime)) { 363 | return InitError(strprintf("Invalid nStartTime (%s)", vDeploymentParams[1])); 364 | } 365 | if (!ParseInt64(vDeploymentParams[2], &nTimeout)) { 366 | return InitError(strprintf("Invalid nTimeout (%s)", vDeploymentParams[2])); 367 | } 368 | bool found = false; 369 | for (int j=0; j<(int)Consensus::MAX_VERSION_BITS_DEPLOYMENTS; ++j) 370 | { 371 | if (vDeploymentParams[0].compare(VersionBitsDeploymentInfo[j].name) == 0) { 372 | UpdateVersionBitsParameters(Consensus::DeploymentPos(j), nStartTime, nTimeout); 373 | found = true; 374 | LogPrintf("Setting version bits activation parameters for %s to start=%ld, timeout=%ld\n", vDeploymentParams[0], nStartTime, nTimeout); 375 | break; 376 | } 377 | } 378 | if (!found) { 379 | return InitError(strprintf("Invalid deployment (%s)", vDeploymentParams[0])); 380 | } 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /start/setup05.md: -------------------------------------------------------------------------------- 1 | # 如何接入比特币网络以及原理分析 2 | 3 | 以下内容为系统启动过程中,每一步骤的详细分析。 4 | 5 | 6 | ### 第5步,验证钱包数据库完整性(`src/init.cpp::AppInitMain()`) 7 | 8 | 调用钱包接口的 `Verify` 方法,验证钱包数据库。实现类为 `wallet/init.cpp` 文件中的 `WalletInit` ,方法处理流程如下: 9 | 10 | 1. 检查启动参数是否禁止钱包 `-disablewallet`。如果是,则直接返回。 11 | 12 | if (gArgs.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET)) { 13 | return true; 14 | } 15 | 16 | 2. 如果启动参数指定了钱包路径 `-walletdir`,则检查钱包数据库目录是否存在,是否为目录、且是否为常规的的路径。 17 | 18 | if (gArgs.IsArgSet("-walletdir")) { 19 | fs::path wallet_dir = gArgs.GetArg("-walletdir", ""); 20 | boost::system::error_code error; 21 | fs::path canonical_wallet_dir = fs::canonical(wallet_dir, error); 22 | if (error || !fs::exists(wallet_dir)) { 23 | return InitError(strprintf(_("Specified -walletdir \"%s\" does not exist"), wallet_dir.string())); 24 | } else if (!fs::is_directory(wallet_dir)) { 25 | return InitError(strprintf(_("Specified -walletdir \"%s\" is not a directory"), wallet_dir.string())); 26 | } else if (!wallet_dir.is_absolute()) { 27 | return InitError(strprintf(_("Specified -walletdir \"%s\" is a relative path"), wallet_dir.string())); 28 | } 29 | gArgs.ForceSetArg("-walletdir", canonical_wallet_dir.string()); 30 | } 31 | 32 | 3. 从启动参数中取得所有的钱包名称。 33 | 34 | std::vector wallet_files = gArgs.GetArgs("-wallet"); 35 | 36 | 4. 根据启动 `-salvagewallet` 和用户指定的钱包数量设置变量 `salvage_wallet`。 37 | 38 | bool salvage_wallet = gArgs.GetBoolArg("-salvagewallet", false) && wallet_files.size() <= 1; 39 | 40 | 5. `for` 循环检查用户提供的所有的钱包,此处至少有一个默认钱包,所以肯定会至少循环一次。 41 | 42 | 如果用户没有指定启动参数 `-wallet`,则在第三步的第21小步中,调用钱包初始接口对象的 `ParameterInteraction` 方法时,设置启动参数 `-wallet`默认为空字符串,从而在本步时至少创建一个钱包名称为空的默认钱包。 43 | 44 | - 根据钱包名称和钱包存放目录,求出钱包的绝对路径。 45 | 46 | fs::path wallet_path = fs::absolute(wallet_file, GetWalletDir()); 47 | 48 | - 如果某个钱包的名字有重复,则返回初始化错误。 49 | 50 | if (!wallet_paths.insert(wallet_path).second) { 51 | return InitError(strprintf(_("Error loading wallet %s. Duplicate -wallet filename specified."), wallet_file)); 52 | } 53 | 54 | - 调用 `CWallet::Verify` 方法,检查钱包。如果出错,则返回错误。 55 | 56 | std::string error_string; 57 | std::string warning_string; 58 | bool verify_success = CWallet::Verify(wallet_file, salvage_wallet, error_string, warning_string); 59 | if (!error_string.empty()) InitError(error_string); 60 | if (!warning_string.empty()) InitWarning(warning_string); 61 | if (!verify_success) return false; 62 | 63 | 这个方法主要是检查钱包的路径方面的。 64 | -------------------------------------------------------------------------------- /start/setup06.md: -------------------------------------------------------------------------------- 1 | # 如何接入比特币网络以及原理分析 2 | 3 | 以下内容为系统启动过程中,每一步骤的详细分析。 4 | 5 | 6 | ### 第6步,网络初始化(`src/init.cpp::AppInitMain()`) 7 | 8 | 1. 生成智能指针对象 g_connman,类型为 `CConnman`。 9 | 10 | g_connman = std::unique_ptr(new CConnman(GetRand(std::numeric_limits::max()), GetRand(std::numeric_limits::max()))); 11 | CConnman& connman = *g_connman; 12 | 13 | 2. 生成智能指针对象 peerLogic,类型为 `PeerLogicValidation`。 14 | 15 | peerLogic.reset(new PeerLogicValidation(&connman, scheduler, gArgs.GetBoolArg("-enablebip61", DEFAULT_ENABLE_BIP61))); 16 | 17 | PeerLogicValidation 继承了 CValidationInterface、NetEventsInterface 两个类。实现 CValidationInterface 这个类可以订阅验证过程中产生的事件。实现 NetEventsInterface 这个类可以处理消息网络消息。 18 | 19 | 3. 注册各种验证处理器,即信号处理器,在发送信号时会调用这些处理器。 20 | 21 | RegisterValidationInterface(peerLogic.get()); 22 | 23 | 方法具体实现如下: 24 | 25 | void RegisterValidationInterface(CValidationInterface* pwalletIn) { 26 | g_signals.m_internals->UpdatedBlockTip.connect(boost::bind(&CValidationInterface::UpdatedBlockTip, pwalletIn, _1, _2, _3)); 27 | g_signals.m_internals->TransactionAddedToMempool.connect(boost::bind(&CValidationInterface::TransactionAddedToMempool, pwalletIn, _1)); 28 | g_signals.m_internals->BlockConnected.connect(boost::bind(&CValidationInterface::BlockConnected, pwalletIn, _1, _2, _3)); 29 | g_signals.m_internals->BlockDisconnected.connect(boost::bind(&CValidationInterface::BlockDisconnected, pwalletIn, _1)); 30 | g_signals.m_internals->TransactionRemovedFromMempool.connect(boost::bind(&CValidationInterface::TransactionRemovedFromMempool, pwalletIn, _1)); 31 | g_signals.m_internals->ChainStateFlushed.connect(boost::bind(&CValidationInterface::ChainStateFlushed, pwalletIn, _1)); 32 | g_signals.m_internals->Broadcast.connect(boost::bind(&CValidationInterface::ResendWalletTransactions, pwalletIn, _1, _2)); 33 | g_signals.m_internals->BlockChecked.connect(boost::bind(&CValidationInterface::BlockChecked, pwalletIn, _1, _2)); 34 | g_signals.m_internals->NewPoWValidBlock.connect(boost::bind(&CValidationInterface::NewPoWValidBlock, pwalletIn, _1, _2)); 35 | } 36 | 37 | 静态变量 g_signals 在程序启动前生成,m_internals 在第4a 步应用程序初始化过程中生成。 38 | 39 | 4. 根据命令行参数 `-uacomment`,处理追加到用户代理的字符串。 40 | 41 | std::vector uacomments; 42 | for (const std::string& cmt : gArgs.GetArgs("-uacomment")) { 43 | if (cmt != SanitizeString(cmt, SAFE_CHARS_UA_COMMENT)) 44 | return InitError(strprintf(_("User Agent comment (%s) contains unsafe characters."), cmt)); 45 | uacomments.push_back(cmt); 46 | } 47 | 48 | 5. 构造并检查版本字符串长度是否大于 `version` 消息中版本的最大长度。 49 | 50 | strSubVersion = FormatSubVersion(CLIENT_NAME, CLIENT_VERSION, uacomments); 51 | if (strSubVersion.size() > MAX_SUBVERSION_LENGTH) { 52 | return InitError(strprintf(_("Total length of network version string (%i) exceeds maximum length (%i). Reduce the number or size of uacomments."), 53 | strSubVersion.size(), MAX_SUBVERSION_LENGTH)); 54 | } 55 | 56 | 6. 如果指定了 `onlynet` 参数,则设置可以对接进行连接的类型,比如:ipv4、ipv6、onion。 57 | 58 | if (gArgs.IsArgSet("-onlynet")) { 59 | std::set nets; 60 | for (const std::string& snet : gArgs.GetArgs("-onlynet")) { 61 | enum Network net = ParseNetwork(snet); 62 | if (net == NET_UNROUTABLE) 63 | return InitError(strprintf(_("Unknown network specified in -onlynet: '%s'"), snet)); 64 | nets.insert(net); 65 | } 66 | for (int n = 0; n < NET_MAX; n++) { 67 | enum Network net = (enum Network)n; 68 | if (!nets.count(net)) 69 | SetLimited(net); 70 | } 71 | } 72 | 73 | 上面的代码首先把 `-onlynet` 参数指定的只允许对外连接的网络类型加入集合中,然后进行 for 遍历,如果当前的类型不在允许的集合中,则调用 `SetLimited` 方法,设置这些类型为受限的。 74 | 75 | 7. 获取是否允许进行 DNS 查找,是否进行代理随机 76 | 77 | fNameLookup = gArgs.GetBoolArg("-dns", DEFAULT_NAME_LOOKUP); 78 | bool proxyRandomize = gArgs.GetBoolArg("-proxyrandomize", DEFAULT_PROXYRANDOMIZE); 79 | 80 | 两者默认都为真。 81 | 82 | 8. 处理网络代理。 83 | 84 | 如果指定了 `-proxy`,且不等于 0,即指定了代理地址,进行下面的处理: 85 | 86 | - 调用 `Lookup` 方法,根据指定的代理,通过 DNS查找,发现代理服务器的地址。 87 | 88 | - 生成 proxyType 对象。 89 | 90 | - 设置 IPv4、IPv6、Tor 网络的代理。 91 | 92 | - 设置命名(域名)代理。 93 | 94 | - 设置不限制连接到 Tor 网络。 95 | 96 | 具体代码如下: 97 | 98 | std::string proxyArg = gArgs.GetArg("-proxy", ""); 99 | SetLimited(NET_ONION); 100 | if (proxyArg != "" && proxyArg != "0") { 101 | CService proxyAddr; 102 | if (!Lookup(proxyArg.c_str(), proxyAddr, 9050, fNameLookup)) { 103 | return InitError(strprintf(_("Invalid -proxy address or hostname: '%s'"), proxyArg)); 104 | } 105 | 106 | proxyType addrProxy = proxyType(proxyAddr, proxyRandomize); 107 | if (!addrProxy.IsValid()) 108 | return InitError(strprintf(_("Invalid -proxy address or hostname: '%s'"), proxyArg)); 109 | 110 | SetProxy(NET_IPV4, addrProxy); 111 | SetProxy(NET_IPV6, addrProxy); 112 | SetProxy(NET_ONION, addrProxy); 113 | SetNameProxy(addrProxy); 114 | SetLimited(NET_ONION, false); // by default, -proxy sets onion as reachable, unless -noonion later 115 | } 116 | 117 | 9. 处理洋葱网络。 如果指定了 `onion` 参数,则处理洋葱网络的相关设置。 118 | 119 | 如果指定了 `-onion`,且不等于空字符串,即指定了洋葱代理地址,进行下面的处理: 120 | 121 | - 如果参数等于 0,设置洋葱网络受限,即不可达。否则,进行下面的处理。 122 | 123 | - 调用 `Lookup` 方法,根据指定的代理,通过 DNS查找,发现代理服务器的地址。 124 | 125 | - 生成 proxyType 对象。 126 | 127 | - 设置 Tor 网络的代理。 128 | 129 | - 设置不限制连接到 Tor 网络。 130 | 131 | 具体代码如下: 132 | 133 | std::string onionArg = gArgs.GetArg("-onion", ""); 134 | if (onionArg != "") { 135 | if (onionArg == "0") { // Handle -noonion/-onion=0 136 | SetLimited(NET_ONION); // set onions as unreachable 137 | } else { 138 | CService onionProxy; 139 | if (!Lookup(onionArg.c_str(), onionProxy, 9050, fNameLookup)) { 140 | return InitError(strprintf(_("Invalid -onion address or hostname: '%s'"), onionArg)); 141 | } 142 | proxyType addrOnion = proxyType(onionProxy, proxyRandomize); 143 | if (!addrOnion.IsValid()) 144 | return InitError(strprintf(_("Invalid -onion address or hostname: '%s'"), onionArg)); 145 | SetProxy(NET_ONION, addrOnion); 146 | SetLimited(NET_ONION, false); 147 | } 148 | } 149 | 150 | 10. 处理通过 `-externalip` 参数设置的外部 IP地址。 151 | 152 | 获取并遍历所有指定的外部地址,进行如下处理:调用 `Lookup` 方法进行DNS 查找。如果成功则调用 `AddLocal` 方法,保存新的地址。否则,抛出初始化错误。 153 | 154 | for (const std::string& strAddr : gArgs.GetArgs("-externalip")) { 155 | CService addrLocal; 156 | if (Lookup(strAddr.c_str(), addrLocal, GetListenPort(), fNameLookup) && addrLocal.IsValid()) 157 | AddLocal(addrLocal, LOCAL_MANUAL); 158 | else 159 | return InitError(ResolveErrMsg("externalip", strAddr)); 160 | } 161 | 162 | 11. 如果设置了 `maxuploadtarget` 参数,则设置最大出站限制。 163 | 164 | if (gArgs.IsArgSet("-maxuploadtarget")) { 165 | nMaxOutboundLimit = gArgs.GetArg("-maxuploadtarget", DEFAULT_MAX_UPLOAD_TARGET)*1024*1024; 166 | } 167 | 168 | -------------------------------------------------------------------------------- /start/setup07.md: -------------------------------------------------------------------------------- 1 | # 如何接入比特币网络以及原理分析 2 | 3 | 以下内容为系统启动过程中,每一步骤的详细分析。 4 | 5 | 6 | ### 第7步,加载区块链(`src/init.cpp::AppInitMain()`) 7 | 8 | 首先,计算缓存的大小。包括:区块索引数据库、区块状态数据库、内存中 UTXO 集。代码如下: 9 | 10 | fReindex = gArgs.GetBoolArg("-reindex", false); 11 | bool fReindexChainState = gArgs.GetBoolArg("-reindex-chainstate", false); 12 | 13 | // cache size calculations 14 | int64_t nTotalCache = (gArgs.GetArg("-dbcache", nDefaultDbCache) << 20); 15 | nTotalCache = std::max(nTotalCache, nMinDbCache << 20); // total cache cannot be less than nMinDbCache 16 | nTotalCache = std::min(nTotalCache, nMaxDbCache << 20); // total cache cannot be greater than nMaxDbcache 17 | int64_t nBlockTreeDBCache = std::min(nTotalCache / 8, nMaxBlockDBCache << 20); 18 | nTotalCache -= nBlockTreeDBCache; 19 | int64_t nTxIndexCache = std::min(nTotalCache / 8, gArgs.GetBoolArg("-txindex", DEFAULT_TXINDEX) ? nMaxTxIndexCache << 20 : 0); 20 | nTotalCache -= nTxIndexCache; 21 | int64_t nCoinDBCache = std::min(nTotalCache / 2, (nTotalCache / 4) + (1 << 23)); // use 25%-50% of the remainder for disk cache 22 | nCoinDBCache = std::min(nCoinDBCache, nMaxCoinsDBCache << 20); // cap total coins db cache 23 | nTotalCache -= nCoinDBCache; 24 | nCoinCacheUsage = nTotalCache; // the rest goes to in-memory cache 25 | int64_t nMempoolSizeMax = gArgs.GetArg("-maxmempool", DEFAULT_MAX_MEMPOOL_SIZE) * 1000000; 26 | LogPrintf("Cache configuration:\n"); 27 | LogPrintf("* Using %.1fMiB for block index database\n", nBlockTreeDBCache * (1.0 / 1024 / 1024)); 28 | if (gArgs.GetBoolArg("-txindex", DEFAULT_TXINDEX)) { 29 | LogPrintf("* Using %.1fMiB for transaction index database\n", nTxIndexCache * (1.0 / 1024 / 1024)); 30 | } 31 | 32 | 上面代码中首先计算总的缓存大小,其用 nTotalCache 表示,通过-dbcache参数设置,然后这个值要取在 nMinDbCache 和 nMaxDbCache 之间。接下来计算 nBlockTreeDBCache 和 nCoinDBCache 以及 nCoinCacheUsage,并且 `nTotalCache = nBlockTreeDBCache +nCoinDBCache + nCoinCacheUsage` 。 33 | 34 | 然后,只要加载标志为真且没有收到关闭系统的请求,即进行以下 while 循环。 35 | 36 | - 调用 `UnloadBlockIndex` 方法,卸载区块相关的索引。 37 | 38 | 因为循环可能执行多次,所以在每次循环开始都要调用本方法来清除相关的一些变量。 39 | 40 | 主要设置包括:活跃区块链的栈顶指针为空、pindexBestInvalid 为空、pindexBestHeader 为空、清空交易池、mapBlocksUnlinked(键为缺少交易的区块索引,值为有对应交易的区块索引)集合为空、vinfoBlockFile 集合为空、最后区块文件句柄 nLastBlockFile 为0、脏区块 setDirtyBlockIndex 集合为空、脏区块文件(句柄)setDirtyFileInfo 集合为空、versionbitscache 缓存清空、清空区块索引集合 mapBlockIndex,等等。 41 | 42 | - 重置指向活跃 CCoinsView 的全局智能指针变量 pcoinsTip。 43 | 44 | - 重置指向 coins 数据库的全局智能指针变量 pcoinsdbview。 45 | 46 | - 重置 CCoinsViewErrorCatcher 的智能指针静态变量 pcoinscatcher。 47 | 48 | - 重置指向活跃区块树的全局智能指针变量 pblocktree,并生成新的对象。 49 | 50 | 这个对象会读写 `/blocks/index/*` 下面的区块文件。 51 | 52 | - 如果 `-reset` 参数为真,那么: 53 | 54 | 调用 pblocktree 的 `WriteReindexing` 方法,向数据库中写入数据。并且进一步,如果当前处于修剪模式,调用 `CleanupBlockRevFiles` 方法,清除特定区块的数据文件。 55 | 56 | if (fReset) { 57 | pblocktree->WriteReindexing(true); 58 | //If we're reindexing in prune mode, wipe away unusable block files and all undo data files 59 | if (fPruneMode) 60 | CleanupBlockRevFiles(); 61 | } 62 | 63 | `WriteReindexing` 方法根据参数值是否为真调用不同方法进行写入。如果参数为真,那么调用 `Write` 方法写入数据;否则调用 `Erase` 方法清除数据。 64 | 65 | `CleanupBlockRevFiles` 方法,首先获取区块的目录,然后用迭代器遍历区块目录。如果当前区块目录是一个常规文件,并且文件名字长度为12,且后缀为 `.dat`,那么进行处理:如果当前文件为区块文件,即 `blk?????.dat` 文件,则放入 mapBlockFiles 集合中,否则,即 `rev?????.dat` 文件,调用 `remove` 方法进行删除。 66 | 67 | 遍历 mapBlockFiles 集合,如果当前区块文件的名字不等于变量 nContigCounter 的值,即区块文件名字出现了不连续,那么就删除指定的区块文件。看代码可能更清楚: 68 | 69 | int nContigCounter = 0; 70 | for (const std::pair& item : mapBlockFiles) { 71 | if (atoi(item.first) == nContigCounter) { 72 | nContigCounter++; 73 | continue; 74 | } 75 | remove(item.second); 76 | } 77 | 78 | - 如果收到结束请求,则退出循环。 79 | 80 | - 调用 `LoadBlockIndex` 方法,加载区块索引。 81 | 82 | if (!LoadBlockIndex(chainparams)) { 83 | strLoadError = _("Error loading block database"); 84 | break; 85 | } 86 | 87 | `LoadBlockIndex` 方法里,如果没有设置 `fReindex`,即不重建区块索引,那么调用 `LoadBlockIndexDB` 方法,从数据库文件加载区块索引,即从 `/blocks/index/*` 文件中加载区块。如果加载成功,那么设置区块索引集合 `mapBlockIndex` 为空。如果设置了 `fReindex` 变量,因为以后会进行重建索引,所以这里没有必要先加载索引了。 88 | 89 | bool LoadBlockIndex(const CChainParams& chainparams) 90 | { 91 | // Load block index from databases 92 | bool needs_init = fReindex; 93 | if (!fReindex) { 94 | bool ret = LoadBlockIndexDB(chainparams); 95 | if (!ret) return false; 96 | needs_init = mapBlockIndex.empty(); 97 | } 98 | 99 | if (needs_init) { 100 | // Everything here is for *new* reindex/DBs. Thus, though 101 | // LoadBlockIndexDB may have set fReindex if we shut down 102 | // mid-reindex previously, we don't check fReindex and 103 | // instead only check it prior to LoadBlockIndexDB to set 104 | // needs_init. 105 | 106 | LogPrintf("Initializing databases...\n"); 107 | } 108 | return true; 109 | } 110 | 111 | - 如果区块索引成功加载,则检查是否包含创世区块。 112 | 113 | if (!mapBlockIndex.empty() && !LookupBlockIndex(chainparams.GetConsensus().hashGenesisBlock)) { 114 | return InitError(_("Incorrect or no genesis block found. Wrong datadir for network?")); 115 | } 116 | 117 | - 如果某些区块被修剪过(即用户手动删除过某些区块),但又没有处于修剪模式,则退出循环。 118 | 119 | if (fHavePruned && !fPruneMode) { 120 | strLoadError = _("You need to rebuild the database using -reindex to go back to unpruned mode. This will redownload the entire blockchain"); 121 | break; 122 | } 123 | 124 | - 如果不重建索引,调用 `LoadGenesisBlock` 加载创世区块。如果失败,则退出循环。 125 | 126 | if (!fReindex && !LoadGenesisBlock(chainparams)) { 127 | strLoadError = _("Error initializing block database"); 128 | break; 129 | } 130 | 131 | `LoadGenesisBlock` 方法内部调用 `CChainState::LoadGenesisBlock` 方法进行处理。后者处理逻辑如下: 132 | 133 | - 首先,检查区块索引集合 `mapBlockIndex` 集合中是否包含创世区块。如果包含则支持返回。 134 | 135 | - 然后,调用区块链参数的 `GenesisBlock` 方法,返回其保存的创世区块,并调用强制转化为区块对象。 136 | 137 | - 再然后,调用 `SaveBlockToDisk` 方法,把创世区块保存到硬盘上。 138 | 139 | - 再然后,调用 `AddToBlockIndex` 方法,返回或生成并返回一个区块索引对象 CBlockIndex。 140 | 141 | - 最后,调用 `ReceivedBlockTransactions` 方法,更新区块的相关信息到区块索引对象上。同时使用深度优先搜索方法寻找当前链的所有可能的下一个区块。 142 | 143 | 下面我们看下 `ReceivedBlockTransactions` 方法: 144 | 145 | - 设置区块索引对象的交易数量为区块的交易集合的大小。 146 | 147 | - 设置区块索引对象的 nChainTx 为0。 148 | 149 | nChainTx 表示从创世区块到当前区块总共有多少个交易,如果这个值为不等于0,那么说明所有父亲都是有效的。 150 | 151 | - 设置区块索引对象的 nFile ,表示区块所在的具体区块文件。 152 | 153 | - 设置区块索引对象的 nDataPos,表示区块在区块文件中偏移的位置。 154 | 155 | - 设置区块索引对象的 nUndoPos,表示区块在 rev?????.dat 文件中偏移的位置。 156 | 157 | - 设置区块索引对象的 nStatus,表示区块完全可用。 158 | 159 | - 调用 `IsWitnessEnabled` 方法,检查是否开启了隔离见证。如果是,则设置区块索引对象的 nStatus,表示区块支持隔离见证。 160 | 161 | - 调用区块索引对象的 `RaiseValidity` 方法,提高此区块索引的有效性级别。 162 | 163 | - 保存区块索引对象到 setDirtyBlockIndex 集合中。 164 | 165 | - 如果是创世区块或当前区块的父区块已经在链上,那么进行下面的处理。 166 | 167 | 生成一个指向区块索引对象指针的队列,然后把当前区块索引放入队列尾部。如果队列不为空,则循环队列处理每一个元素。取得队列中的第一个元素,并从队列中删除。设置当前区块索引对象的 nChainTx 为前一个父区块索引对象的 nChainTx 加上当前区块的交易数量(如果是创世区块,那么父区块索引对象的 nChainTx 为0);设置区块索引对象的序列号;如果活跃区块链的顶端区块不空或当前区块索引对象在活跃区块链顶端区块之后,则把当前区块索引对象加入到 setBlockIndexCandidates 集合中;不断的从孤立的区块集合 mapBlocksUnlinked 中查找,并将当前链的所有可能的下一个区块保存到 setBlockIndexCandidates 中。 168 | 169 | void CChainState::ReceivedBlockTransactions(const CBlock& block, CBlockIndex* pindexNew, const CDiskBlockPos& pos, const Consensus::Params& consensusParams) 170 | { 171 | pindexNew->nTx = block.vtx.size(); 172 | pindexNew->nChainTx = 0; 173 | pindexNew->nFile = pos.nFile; 174 | pindexNew->nDataPos = pos.nPos; 175 | pindexNew->nUndoPos = 0; 176 | pindexNew->nStatus |= BLOCK_HAVE_DATA; 177 | if (IsWitnessEnabled(pindexNew->pprev, consensusParams)) { 178 | pindexNew->nStatus |= BLOCK_OPT_WITNESS; 179 | } 180 | pindexNew->RaiseValidity(BLOCK_VALID_TRANSACTIONS); 181 | setDirtyBlockIndex.insert(pindexNew); 182 | 183 | if (pindexNew->pprev == nullptr || pindexNew->pprev->nChainTx) { 184 | // If pindexNew is the genesis block or all parents are BLOCK_VALID_TRANSACTIONS. 185 | std::deque queue; 186 | queue.push_back(pindexNew); 187 | 188 | // Recursively process any descendant blocks that now may be eligible to be connected. 189 | while (!queue.empty()) { 190 | CBlockIndex *pindex = queue.front(); 191 | queue.pop_front(); 192 | pindex->nChainTx = (pindex->pprev ? pindex->pprev->nChainTx : 0) + pindex->nTx; 193 | { 194 | LOCK(cs_nBlockSequenceId); 195 | pindex->nSequenceId = nBlockSequenceId++; 196 | } 197 | if (chainActive.Tip() == nullptr || !setBlockIndexCandidates.value_comp()(pindex, chainActive.Tip())) { 198 | setBlockIndexCandidates.insert(pindex); 199 | } 200 | std::pair::iterator, std::multimap::iterator> range = mapBlocksUnlinked.equal_range(pindex); 201 | while (range.first != range.second) { 202 | std::multimap::iterator it = range.first; 203 | queue.push_back(it->second); 204 | range.first++; 205 | mapBlocksUnlinked.erase(it); 206 | } 207 | } 208 | } else { 209 | if (pindexNew->pprev && pindexNew->pprev->IsValid(BLOCK_VALID_TREE)) { 210 | mapBlocksUnlinked.insert(std::make_pair(pindexNew->pprev, pindexNew)); 211 | } 212 | } 213 | } 214 | 215 | - 生成两个智能指针对象。 216 | 217 | pcoinsdbview.reset(new CCoinsViewDB(nCoinDBCache, false, fReset || fReindexChainState)); 218 | pcoinscatcher.reset(new CCoinsViewErrorCatcher(pcoinsdbview.get())); 219 | 220 | 程序执行到这里,要么是已经重新建立所有区块索引;要么已经将所有区块的索引加载到 mapBlockIndex 中了。 221 | 222 | - 升级数据库格式。 223 | 224 | if (!pcoinsdbview->Upgrade()) { 225 | strLoadError = _("Error upgrading chainstate database"); 226 | break; 227 | } 228 | 229 | - 接下来处理分叉引起的数据库不一致。 230 | 231 | if (!ReplayBlocks(chainparams, pcoinsdbview.get())) { 232 | strLoadError = _("Unable to replay blocks. You will need to rebuild the database using -reindex-chainstate."); 233 | break; 234 | } 235 | 236 | `ReplayBlocks` 函数内部调用 `CChainState` 的同名方法进行处理。后者处理逻辑如下: 237 | 238 | - 生成 CCoinsViewCache 的缓存对象 cache 来缓存 CCoinsView。 239 | 240 | - 调用 `CCoinsView` 子类 `CCoinsViewDB` 的 `GetHeadBlocks` 方法,从数据库中加载 Key 为 `H` 的区块头部哈希,并保存在集合中。 241 | 242 | - 如果前一部的区块头部哈希集合为空,直接返回真。如果集合长度不等于2,则抛出异常。 243 | 244 | - 如果区块索引集合 mapBlockIndex 不包含区块头部哈希集合中第1个,则抛出异常。否则,保存在 pindexNew 变量中。 245 | 246 | - 如果区块头部哈希集合中第2个的内容不为0,进行如下的检查:如果这个值不在区块索引集合 mapBlockIndex 中,则抛出异常。否则,保存在 pindexOld 变量中,调用 `LastCommonAncestor` 方法,查到 pindexOld 和 pindexNew 这两个区块的最后一个共同祖先,并保存为 pindexFork。 247 | 248 | - 然后,开始回滚到旧分支。只要 pindexOld 不等于最后一个共同祖先,就进行循环处理。具体逻辑如下:首先,如果当前区块索引对象的高度大于0,即不是创世区块,那么调用 `ReadBlockFromDisk` 方法,从硬盘加载指定的区块,然后调用 `DisconnectBlock` 方法,断开这个区块连接;其次,保存当前区块索引对象的前父对象为当前索引对象。 249 | 250 | - 最后,从最后一个共同祖先向前滚动到区块链的顶端。 251 | 252 | 以上处理说起来可能不是太清楚,下面是具体代码,可以参考代码进行理解。 253 | 254 | bool CChainState::ReplayBlocks(const CChainParams& params, CCoinsView* view) 255 | { 256 | LOCK(cs_main); 257 | 258 | CCoinsViewCache cache(view); 259 | 260 | std::vector hashHeads = view->GetHeadBlocks(); 261 | if (hashHeads.empty()) return true; // We're already in a consistent state. 262 | if (hashHeads.size() != 2) return error("ReplayBlocks(): unknown inconsistent state"); 263 | 264 | uiInterface.ShowProgress(_("Replaying blocks..."), 0, false); 265 | LogPrintf("Replaying blocks\n"); 266 | 267 | const CBlockIndex* pindexOld = nullptr; // Old tip during the interrupted flush. 268 | const CBlockIndex* pindexNew; // New tip during the interrupted flush. 269 | const CBlockIndex* pindexFork = nullptr; // Latest block common to both the old and the new tip. 270 | 271 | if (mapBlockIndex.count(hashHeads[0]) == 0) { 272 | return error("ReplayBlocks(): reorganization to unknown block requested"); 273 | } 274 | pindexNew = mapBlockIndex[hashHeads[0]]; 275 | 276 | if (!hashHeads[1].IsNull()) { // The old tip is allowed to be 0, indicating it's the first flush. 277 | if (mapBlockIndex.count(hashHeads[1]) == 0) { 278 | return error("ReplayBlocks(): reorganization from unknown block requested"); 279 | } 280 | pindexOld = mapBlockIndex[hashHeads[1]]; 281 | pindexFork = LastCommonAncestor(pindexOld, pindexNew); 282 | assert(pindexFork != nullptr); 283 | } 284 | 285 | // Rollback along the old branch. 286 | while (pindexOld != pindexFork) { 287 | if (pindexOld->nHeight > 0) { // Never disconnect the genesis block. 288 | CBlock block; 289 | if (!ReadBlockFromDisk(block, pindexOld, params.GetConsensus())) { 290 | return error("RollbackBlock(): ReadBlockFromDisk() failed at %d, hash=%s", pindexOld->nHeight, pindexOld->GetBlockHash().ToString()); 291 | } 292 | LogPrintf("Rolling back %s (%i)\n", pindexOld->GetBlockHash().ToString(), pindexOld->nHeight); 293 | DisconnectResult res = DisconnectBlock(block, pindexOld, cache); 294 | if (res == DISCONNECT_FAILED) { 295 | return error("RollbackBlock(): DisconnectBlock failed at %d, hash=%s", pindexOld->nHeight, pindexOld->GetBlockHash().ToString()); 296 | } 297 | // If DISCONNECT_UNCLEAN is returned, it means a non-existing UTXO was deleted, or an existing UTXO was 298 | // overwritten. It corresponds to cases where the block-to-be-disconnect never had all its operations 299 | // applied to the UTXO set. However, as both writing a UTXO and deleting a UTXO are idempotent operations, 300 | // the result is still a version of the UTXO set with the effects of that block undone. 301 | } 302 | pindexOld = pindexOld->pprev; 303 | } 304 | 305 | // Roll forward from the forking point to the new tip. 306 | int nForkHeight = pindexFork ? pindexFork->nHeight : 0; 307 | for (int nHeight = nForkHeight + 1; nHeight <= pindexNew->nHeight; ++nHeight) { 308 | const CBlockIndex* pindex = pindexNew->GetAncestor(nHeight); 309 | LogPrintf("Rolling forward %s (%i)\n", pindex->GetBlockHash().ToString(), nHeight); 310 | if (!RollforwardBlock(pindex, cache, params)) return false; 311 | } 312 | 313 | cache.SetBestBlock(pindexNew->GetBlockHash()); 314 | cache.Flush(); 315 | uiInterface.ShowProgress("", 100, false); 316 | return true; 317 | } 318 | 319 | - 当系统走到这一步时,硬盘上的 coinsdb 数据库已经片于一致状态了。现在可以创建指向活跃 CCoinsView 的全局智能指针变量 pcoinsTip。 320 | 321 | pcoinsTip.reset(new CCoinsViewCache(pcoinscatcher.get())); 322 | 323 | - 如果 coins 视图不空,那么加载指向最佳区块链的栈顶区块,即最高区块。 324 | 325 | bool is_coinsview_empty = fReset || fReindexChainState || pcoinsTip->GetBestBlock().IsNull(); 326 | if (!is_coinsview_empty) { 327 | // LoadChainTip sets chainActive based on pcoinsTip's best block 328 | if (!LoadChainTip(chainparams)) { 329 | strLoadError = _("Error initializing block database"); 330 | break; 331 | } 332 | assert(chainActive.Tip() != nullptr); 333 | } 334 | 335 | 在 `LoadChainTip` 方法中,首先检查活跃区块链的顶端区块,如果存在且其哈希等于 pcoinsTip 指向的最佳区块,即最佳区块链/活跃区块链的顶端区块存在,直接返回真。否则,调用活跃 coins 视图 pcoinsTip 的 `GetBestBlock` 方法返回最佳区块,如果最佳区块不为空并且当前区块链只有创世区块(即区块映射集合长度等于1),调用 `ActivateBestChain` 方法,激活最佳区块链。调用 `LookupBlockIndex` 方法,查找最佳区块的索引。如果可以找到,则设置为活跃区块链的顶端指示区块。 336 | 337 | 具体代码如下所示: 338 | 339 | bool LoadChainTip(const CChainParams& chainparams) 340 | { 341 | AssertLockHeld(cs_main); 342 | 343 | if (chainActive.Tip() && chainActive.Tip()->GetBlockHash() == pcoinsTip->GetBestBlock()) return true; 344 | 345 | if (pcoinsTip->GetBestBlock().IsNull() && mapBlockIndex.size() == 1) { 346 | // In case we just added the genesis block, connect it now, so 347 | // that we always have a chainActive.Tip() when we return. 348 | LogPrintf("%s: Connecting genesis block...\n", __func__); 349 | CValidationState state; 350 | if (!ActivateBestChain(state, chainparams)) { 351 | LogPrintf("%s: failed to activate chain (%s)\n", __func__, FormatStateMessage(state)); 352 | return false; 353 | } 354 | } 355 | 356 | // Load pointer to end of best chain 357 | CBlockIndex* pindex = LookupBlockIndex(pcoinsTip->GetBestBlock()); 358 | if (!pindex) { 359 | return false; 360 | } 361 | chainActive.SetTip(pindex); 362 | 363 | g_chainstate.PruneBlockIndexCandidates(); 364 | 365 | LogPrintf("Loaded best chain: hashBestChain=%s height=%d date=%s progress=%f\n", 366 | chainActive.Tip()->GetBlockHash().ToString(), chainActive.Height(), 367 | FormatISO8601DateTime(chainActive.Tip()->GetBlockTime()), 368 | GuessVerificationProgress(chainparams.TxData(), chainActive.Tip())); 369 | return true; 370 | } 371 | 372 | - 接下来处理数据缺失的情况。 373 | 374 | if (!fReset) { 375 | uiInterface.InitMessage(_("Rewinding blocks...")); 376 | if (!RewindBlockIndex(chainparams)) { 377 | strLoadError = _("Unable to rewind the database to a pre-fork state. You will need to redownload the blockchain"); 378 | break; 379 | } 380 | } 381 | 382 | `RewindBlockIndex` 方法中,会断开区块连接,并从区块索引对象中删除相关的区块索引。 383 | 384 | - 最后,进行区块数据验证。具体处理可以看代码,此处不细说。 385 | 386 | -------------------------------------------------------------------------------- /start/setup08.md: -------------------------------------------------------------------------------- 1 | # 如何接入比特币网络以及原理分析 2 | 3 | 以下内容为系统启动过程中,每一步骤的详细分析。 4 | 5 | 6 | ### 第8步,建立索引(`src/init.cpp::AppInitMain()`) 7 | 8 | 如果指定了 `-txindex` 参数,则生成交易索引对象 g_txindex,类型为 `TxIndex`;然后调用其 `Start` 方法,开始建立索引。 9 | 10 | if (gArgs.GetBoolArg("-txindex", DEFAULT_TXINDEX)) { 11 | g_txindex = MakeUnique(nTxIndexCache, false, fReindex); 12 | g_txindex->Start(); 13 | } 14 | 15 | `start` 方法处理如下: 16 | 17 | 1. 首先,调用 `RegisterValidationInterface` 方法注册 `TxIndex` 为 `MainSignalsInstance` 上各种事件的信号处理器,在发送信号时会调用这些处理器。 18 | 19 | RegisterValidationInterface(this); 20 | 21 | 2. 然后,调用 `Init` 方法升级交易索引从老的数据库到新的数据库。 22 | 23 | `TxIndex` 子类重载了这个方法,会调用 `m_db->MigrateData(*pblocktree, chainActive.GetLocator())` 方法来升级数据库。 24 | 25 | 然后,调用父类 `BaseIndex` 的同名方法进行处理。在父类的 `Init` 方法中,首先会调用 `ReadBestBlock` 方法从数据库中读取 Key 为 `B` 的区块做为定位器(可能是所有没有分叉的区块)。然后,调用 `FindForkInGlobalIndex` 方法,找到活跃区块链上的分叉前的最后一区块索引(从这个区块产生了分叉)。如果这个索引对应的区块和活跃区块链的顶端区块是相同的,设置同步完成标志为真。 26 | 27 | 3. 启动一个线程,线程执行的真正方法为 `BaseIndex::ThreadSync`。线程的主要作用在于当没有同步完成时,通过读取活跃区块链的下一个区块来进行同步,并把没有分叉的区块以 Key 为 `B` 写入数据库中。 28 | 29 | -------------------------------------------------------------------------------- /start/setup09.md: -------------------------------------------------------------------------------- 1 | # 如何接入比特币网络以及原理分析 2 | 3 | 以下内容为系统启动过程中,每一步骤的详细分析。 4 | 5 | 6 | ### 第9步,加载钱包(`src/init.cpp::AppInitMain()`) 7 | 8 | 调用钱包接口对象的 `Open` 方法,开始加载钱包。实现类为 `wallet/init.cpp` 文件中的 `Open` ,方法处理流程如下: 9 | 10 | 11 | 1. 检查启动参数是否禁止钱包 `-disablewallet`。如果是,则直接返回。 12 | 13 | if (gArgs.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET)) { 14 | return true; 15 | } 16 | 17 | 2. `for` 循环用户提供的所有的钱包,此处至少有一个默认钱包,所以肯定会至少循环一次。 18 | 19 | 如果用户没有指定启动参数 `-wallet`,则在第三步的第21小步中,调用钱包初始接口对象的 `ParameterInteraction` 方法时,设置启动参数 `-wallet`默认为空字符串,从而在本步时至少创建一个钱包名称为空的默认钱包。 20 | 21 | - 调用 `CreateWalletFromFile` 方法,创建钱包。 22 | 23 | std::shared_ptr pwallet = CWallet::CreateWalletFromFile(walletFile, fs::absolute(walletFile, GetWalletDir())); 24 | 25 | - 如果创建钱包不成功,则返回假。 26 | 27 | if (!pwallet) { 28 | return false; 29 | } 30 | 31 | - 调用 `AddWallet` 方法,把生成的钱包加入钱包集合 `vpwallets` 中。 32 | 33 | 3. 返回真。 34 | 35 | 36 | `CreateWalletFromFile` 方法的具体讲解会在密钥、地址和钱包部分进行详细的分析,此处略过不讲。 37 | 38 | 通过本步骤,系统建立了我们的第一个默认的钱包。 39 | -------------------------------------------------------------------------------- /start/setup10.md: -------------------------------------------------------------------------------- 1 | # 如何接入比特币网络以及原理分析 2 | 3 | 以下内容为系统启动过程中,每一步骤的详细分析。 4 | 5 | 6 | ### 第10,数据目录维护(`src/init.cpp::AppInitMain()`) 7 | 8 | 如果当前为修剪模式,本地服务去掉 `NODE_NETWORK` 标志,然后如果不需要索引则调用 `PruneAndFlush` 函数,修剪并刷新到硬盘中。 9 | 10 | if (fPruneMode) { 11 | LogPrintf("Unsetting NODE_NETWORK on prune mode\n"); 12 | nLocalServices = ServiceFlags(nLocalServices & ~NODE_NETWORK); 13 | if (!fReindex) { 14 | uiInterface.InitMessage(_("Pruning blockstore...")); 15 | PruneAndFlush(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /start/setup11.md: -------------------------------------------------------------------------------- 1 | # 如何接入比特币网络以及原理分析 2 | 3 | 以下内容为系统启动过程中,每一步骤的详细分析。 4 | 5 | 6 | ### 第11步,导入区块(`src/init.cpp::AppInitMain()`) 7 | 8 | 1. 调用 `CheckDiskSpace` 函数,检查硬盘空间是否足够。 9 | 10 | 如果没有足够的硬盘空间,则退出。 11 | 12 | 2. 检查最佳区块链顶端指示指针是否为空。 13 | 14 | 如果顶端打针为空,UI界面进行通知。如果不空,则设置有创世区块,即 `fHaveGenesis` 设为真。 15 | 16 | if (chainActive.Tip() == nullptr) { 17 | uiInterface.NotifyBlockTip_connect(BlockNotifyGenesisWait); 18 | } else { 19 | fHaveGenesis = true; 20 | } 21 | 22 | 3. 如果指定了 `blocknotify` 参数,设置界面通知为 `BlockNotifyCallback`。 23 | 24 | 4. 遍历参数 `loadblock` 指定要加载的区块文件,放进向量变量 `vImportFiles` 集合中。然后调用 `threadGroup.create_thread` 方法,创建一个线程。线程执行的函数为 `ThreadImport`,参数为要加载的区块文件。 25 | 26 | std::vector vImportFiles; 27 | for (const std::string& strFile : gArgs.GetArgs("-loadblock")) { 28 | vImportFiles.push_back(strFile); 29 | } 30 | 31 | threadGroup.create_thread(boost::bind(&ThreadImport, vImportFiles)); 32 | 33 | 5. 获取 `cs_GenesisWait` 锁,等待创世区块被处理完成。 34 | 35 | { 36 | WaitableLock lock(cs_GenesisWait); 37 | while (!fHaveGenesis && !ShutdownRequested()) { 38 | condvar_GenesisWait.wait_for(lock, std::chrono::milliseconds(500)); 39 | } 40 | uiInterface.NotifyBlockTip_disconnect(BlockNotifyGenesisWait); 41 | } 42 | 43 | -------------------------------------------------------------------------------- /start/setup12.md: -------------------------------------------------------------------------------- 1 | # 如何接入比特币网络以及原理分析 2 | 3 | 以下内容为系统启动过程中,每一步骤的详细分析。 4 | 5 | 6 | ### 第12步,启动节点(`src/init.cpp::AppInitMain()`) 7 | 8 | 1. 获取活跃区块链的当前调度。 9 | 10 | chain_active_height = chainActive.Height(); 11 | 12 | 2. 如果指定了监听洋葱网络 `-listenonion`,调用 `StartTorControl` 函数,开始 Tor 控制。 13 | 14 | 代码如下所示: 15 | 16 | void StartTorControl() 17 | { 18 | assert(!gBase); 19 | #ifdef WIN32 20 | evthread_use_windows_threads(); 21 | #else 22 | evthread_use_pthreads(); 23 | #endif 24 | gBase = event_base_new(); 25 | if (!gBase) { 26 | LogPrintf("tor: Unable to create event_base\n"); 27 | return; 28 | } 29 | 30 | torControlThread = std::thread(std::bind(&TraceThread, "torcontrol", &TorControlThread)); 31 | } 32 | 33 | libevent默认情况下是单线程,每个线程有且仅有一个event_base。为了保存多线程下是安全的,首先需要调用 `evthread_use_pthreads` 、`evthread_use_windows_threads` 等两个方法,前面是 linux 下的,后面是 windows 下的。 34 | 35 | 在处理完多线程设置后,调用 `event_base_new` 方法,创建一个默认的 event_base。 36 | 37 | 最后,启动一个 Tor 控制线程。具体调用 `std::thread` 方法,创建一个线程,线程的具体执行方法为 `std::bind` 返回的绑定函数。标准绑定函数的第一个参数为要执行的函数,此处为 `TraceThread`,第二个参数为线程的名字 `torcontrol`,第三个参数为线程要执行的真正方法,此处为 `TorControlThread` 函数,后面两个参数都会做为参数,传递到第一个函数。 38 | 39 | `TraceThread` 函数,调用 `RenameThread` 方法,把线程名字设置为 `bitcoin-torcontrol`,然后执行传递进来的 `TorControlThread` 函数。后者会生成一个 Tor 控制器,然后调用 `event_base_dispatch` 方法,分发事件。代码如下: 40 | 41 | static void TorControlThread() 42 | { 43 | TorController ctrl(gBase, gArgs.GetArg("-torcontrol", DEFAULT_TOR_CONTROL)); 44 | 45 | event_base_dispatch(gBase); 46 | } 47 | 48 | `TorController` 构造函数中会做几件重要的事情: 49 | 50 | - 首先,调用 `event_new` 方法生成一个 event 对象,event 对象的回调函数为 `reconnect_cb` 。 51 | 52 | - 然后,调用 `TorControlConnection::Connect` 方法连接到 Tor 控制器。 53 | 54 | 这个方法又会做几件事情: 55 | 56 | - 解析 Tor 控制器的地址。 57 | 58 | - 调用 `bufferevent_socket_new` 方法,基于套接字生成一个 bufferevent。 59 | 60 | - 设置 bufferevent 的回调方法,包括:读取回调函数为 `TorControlConnection::readcb`,写入回调函数为空,事件回调函数为 `TorControlConnection::eventcb`,同时指定 bufferevent 启用读写标志。 61 | 62 | - 设置 `TorControlConnection` 连接、断开连接的两个指针函数分别为:`TorController::connected_cb` 和 `TorController::disconnected_cb`。 63 | 64 | - 调用 `bufferevent_socket_connect` 方法,连接到前面生成的 bufferevent。 65 | 66 | 方法在连接成功后,会立即调用事件回调函数 `TorControlConnection::eventcb`。 67 | 68 | 3. 调用 `Discover` 函数,开始发现本节点的地址。 69 | 70 | 方法内首先判断是否已经处理过。如果没有,那么开始发现本节点的地址。具体处理分为 windows 和 linux,下面主要讲述 linux 下的处理。 71 | 72 | 调用 `getifaddrs` 方法,查找系统所有的网络接口的信息,包括以太网卡接口和回环接口等。本方法返回一个如下的结构体: 73 | 74 | struct ifaddrs 75 | { 76 | struct ifaddrs *ifa_next; /* 列表中的下一个条目 */ 77 | char *ifa_name; /* 接口的名称 */ 78 | unsigned int ifa_flags; /* 来自 SIOCGIFFLAGS 的标志 */ 79 | struct sockaddr *ifa_addr; /* 接口的地址 */ 80 | struct sockaddr *ifa_netmask; /* 接口的网络掩码 */ 81 | union 82 | { 83 | struct sockaddr *ifu_broadaddr; /* 接口的广播地址 */ 84 | struct sockaddr *ifu_dstaddr; /* 点对点的目标地址 */ 85 | } ifa_ifu; 86 | #define ifa_broadaddr ifa_ifu.ifu_broadaddr 87 | #define ifa_dstaddr ifa_ifu.ifu_dstaddr 88 | void *ifa_data; /* Address-specific data */ 89 | }; 90 | 91 | 如果可以获取接口信息,则遍历每一个接口,进行如下处理: 92 | 93 | - 如果接口地址为空,则处理下一个。 94 | 95 | - 如果不是接口标志不是 IFF_UP ,则处理下一个。 96 | 97 | - 如果接口名称是 lo 或 lo0,则处理下一个。 98 | 99 | - 如果接口是 tcp,TCP 等,则生成 IP 地址对象,然后调用 `AddLocal` 方法,保存本地地址。 100 | 101 | - 如果接口是 IPV6,则则生成 IP 地址对象,然后调用 `AddLocal` 方法,保存本地地址。 102 | 103 | 代码如下所示: 104 | 105 | if (getifaddrs(&myaddrs) == 0) 106 | { 107 | for (struct ifaddrs* ifa = myaddrs; ifa != nullptr; ifa = ifa->ifa_next) 108 | { 109 | if (ifa->ifa_addr == nullptr) continue; 110 | if ((ifa->ifa_flags & IFF_UP) == 0) continue; 111 | if (strcmp(ifa->ifa_name, "lo") == 0) continue; 112 | if (strcmp(ifa->ifa_name, "lo0") == 0) continue; 113 | if (ifa->ifa_addr->sa_family == AF_INET) 114 | { 115 | struct sockaddr_in* s4 = (struct sockaddr_in*)(ifa->ifa_addr); 116 | CNetAddr addr(s4->sin_addr); 117 | if (AddLocal(addr, LOCAL_IF)) 118 | LogPrintf("%s: IPv4 %s: %s\n", __func__, ifa->ifa_name, addr.ToString()); 119 | } 120 | else if (ifa->ifa_addr->sa_family == AF_INET6) 121 | { 122 | struct sockaddr_in6* s6 = (struct sockaddr_in6*)(ifa->ifa_addr); 123 | CNetAddr addr(s6->sin6_addr); 124 | if (AddLocal(addr, LOCAL_IF)) 125 | LogPrintf("%s: IPv6 %s: %s\n", __func__, ifa->ifa_name, addr.ToString()); 126 | } 127 | } 128 | freeifaddrs(myaddrs); 129 | } 130 | 131 | 4. 如果指定了 `upnp` 参数,则调用 `StartMapPort` 函数,开始进行端口映射。 132 | 133 | if (gArgs.GetBoolArg("-upnp", DEFAULT_UPNP)) { 134 | StartMapPort(); 135 | } 136 | 137 | 5. 生成选项对象,并进行初始化。 138 | 139 | CConnman::Options connOptions; 140 | connOptions.nLocalServices = nLocalServices; 141 | connOptions.nMaxConnections = nMaxConnections; 142 | connOptions.nMaxOutbound = std::min(MAX_OUTBOUND_CONNECTIONS, connOptions.nMaxConnections); 143 | connOptions.nMaxAddnode = MAX_ADDNODE_CONNECTIONS; 144 | connOptions.nMaxFeeler = 1; 145 | connOptions.nBestHeight = chain_active_height; 146 | connOptions.uiInterface = &uiInterface; 147 | connOptions.m_msgproc = peerLogic.get(); 148 | connOptions.nSendBufferMaxSize = 1000*gArgs.GetArg("-maxsendbuffer", DEFAULT_MAXSENDBUFFER); 149 | connOptions.nReceiveFloodSize = 1000*gArgs.GetArg("-maxreceivebuffer", DEFAULT_MAXRECEIVEBUFFER); 150 | connOptions.m_added_nodes = gArgs.GetArgs("-addnode"); 151 | 152 | connOptions.nMaxOutboundTimeframe = nMaxOutboundTimeframe; 153 | connOptions.nMaxOutboundLimit = nMaxOutboundLimit; 154 | 155 | 上面的代码基本就是设置本地支持的服务、最大连接数、最大出站数、最大节点数、最大费率、活跃区块链的高度、节点逻辑验证器、发送的最大缓冲值、接收的最大缓冲值、连接的节点数等。 156 | 157 | 6. 如果指定了 `-bind` 参数,则处理绑定参数。 158 | 159 | for (const std::string& strBind : gArgs.GetArgs("-bind")) { 160 | CService addrBind; 161 | if (!Lookup(strBind.c_str(), addrBind, GetListenPort(), false)) { 162 | return InitError(ResolveErrMsg("bind", strBind)); 163 | } 164 | connOptions.vBinds.push_back(addrBind); 165 | } 166 | 167 | 遍历所有的绑定地址,调用 `Lookup` 方法,进行 DNS查找。如果可以找到对应 IP地址,把生成的 `CService` 对象放入选项对象的 `vBinds` 属性中。 168 | 169 | 7. 如果指定了 `-whitebind` 参数,则处理绑定参数。 170 | 171 | for (const std::string& strBind : gArgs.GetArgs("-whitebind")) { 172 | CService addrBind; 173 | if (!Lookup(strBind.c_str(), addrBind, 0, false)) { 174 | return InitError(ResolveErrMsg("whitebind", strBind)); 175 | } 176 | if (addrBind.GetPort() == 0) { 177 | return InitError(strprintf(_("Need to specify a port with -whitebind: '%s'"), strBind)); 178 | } 179 | connOptions.vWhiteBinds.push_back(addrBind); 180 | } 181 | 182 | 遍历所有的绑定地址,调用 `Lookup` 方法,进行 DNS查找。如果可以找到对应 IP地址,且对应的端口号不等于0,把生成的 `CService` 对象放入选项对象的 `vWhiteBinds` 属性中。 183 | 184 | 8. 如果指定了 `-whitelist` 参数,则处理白名单列表。 185 | 186 | for (const auto& net : gArgs.GetArgs("-whitelist")) { 187 | CSubNet subnet; 188 | LookupSubNet(net.c_str(), subnet); 189 | if (!subnet.IsValid()) 190 | return InitError(strprintf(_("Invalid netmask specified in -whitelist: '%s'"), net)); 191 | connOptions.vWhitelistedRange.push_back(subnet); 192 | } 193 | 194 | 遍历白名单列表,调用 `LookupSubNet` 方法,查找对应的子网掩码,如果对应的子网掩码是有效的,那么放入选项对象的 `vWhitelistedRange` 属性中。 195 | 196 | 9. 取得参数 `seednode` 指定的值,放入选项对象的 `vSeedNodes` 属性中。 197 | 198 | connOptions.vSeedNodes = gArgs.GetArgs("-seednode"); 199 | 200 | 10. 调用 `CConnman` 对象的 `Start` 方法,初始所有的出站连接。 201 | 202 | **本方法非常非常重要,因为它启动了一个重要的流程,即底层的 P2P 网络建立和消息处理流程**。 203 | 204 | 具体分析如下: 205 | 206 | - 调用 `Init` 方法,根据选项对象设置对象的属性,包括:本地支持的服务、最大连接数、最大出站数、最大增加的节点数、最大费率、最佳区块链高度等等。不细说,代码如下: 207 | 208 | void Init(const Options& connOptions) { 209 | nLocalServices = connOptions.nLocalServices; 210 | nMaxConnections = connOptions.nMaxConnections; 211 | nMaxOutbound = std::min(connOptions.nMaxOutbound, connOptions.nMaxConnections); 212 | nMaxAddnode = connOptions.nMaxAddnode; 213 | nMaxFeeler = connOptions.nMaxFeeler; 214 | nBestHeight = connOptions.nBestHeight; 215 | clientInterface = connOptions.uiInterface; 216 | m_msgproc = connOptions.m_msgproc; 217 | nSendBufferMaxSize = connOptions.nSendBufferMaxSize; 218 | nReceiveFloodSize = connOptions.nReceiveFloodSize; 219 | { 220 | LOCK(cs_totalBytesSent); 221 | nMaxOutboundTimeframe = connOptions.nMaxOutboundTimeframe; 222 | nMaxOutboundLimit = connOptions.nMaxOutboundLimit; 223 | } 224 | vWhitelistedRange = connOptions.vWhitelistedRange; 225 | { 226 | LOCK(cs_vAddedNodes); 227 | vAddedNodes = connOptions.m_added_nodes; 228 | } 229 | } 230 | 231 | - 接下来,使用锁初始一些比较重要的属性,包括:设置总接收的字节 `nTotalBytesRecv`、总的发送数量`nTotalBytesSent`、`nMaxOutboundTotalBytesSentInCycle`、`nMaxOutboundCycleStartTime` 等都为0。 232 | 233 | { 234 | LOCK(cs_totalBytesRecv); 235 | nTotalBytesRecv = 0; 236 | } 237 | { 238 | LOCK(cs_totalBytesSent); 239 | nTotalBytesSent = 0; 240 | nMaxOutboundTotalBytesSentInCycle = 0; 241 | nMaxOutboundCycleStartTime = 0; 242 | } 243 | 244 | - 再接下来,获取节点绑定的本地地址和端口,并生成对应的套接字,接受别的节点的请求。 245 | 246 | if (fListen && !InitBinds(connOptions.vBinds, connOptions.vWhiteBinds)) { 247 | if (clientInterface) { 248 | clientInterface->ThreadSafeMessageBox( 249 | _("Failed to listen on any port. Use -listen=0 if you want this."), 250 | "", CClientUIInterface::MSG_ERROR); 251 | } 252 | return false; 253 | } 254 | 255 | `InitBinds` 方法,接收 `-bind` 和 `-whitebind` 参数生成的集合,并解析各个地址,生成套接字,并进行监听。具体分析如下: 256 | 257 | - 首先,处理`-bind` 地址集合。 258 | 259 | for (const auto& addrBind : binds) { 260 | fBound |= Bind(addrBind, (BF_EXPLICIT | BF_REPORT_ERROR)); 261 | } 262 | 263 | - 然后,处理 `-whitebind` 地址集合。 264 | 265 | for (const auto& addrBind : whiteBinds) { 266 | fBound |= Bind(addrBind, (BF_EXPLICIT | BF_REPORT_ERROR | BF_WHITELIST)); 267 | } 268 | 269 | - 如果,两个参数都没有指定,则使用下面代码进行处理。 270 | 271 | if (binds.empty() && whiteBinds.empty()) { 272 | struct in_addr inaddr_any; 273 | inaddr_any.s_addr = INADDR_ANY; 274 | struct in6_addr inaddr6_any = IN6ADDR_ANY_INIT; 275 | fBound |= Bind(CService(inaddr6_any, GetListenPort()), BF_NONE); 276 | fBound |= Bind(CService(inaddr_any, GetListenPort()), !fBound ? BF_REPORT_ERROR : BF_NONE); 277 | } 278 | 279 | 从以上代码可以看出来,三种情况下,处理基本相同,都是调用 `Bind` 方法来处理。下面,我们进进入这个方法一控究竟。这个方法的主体是调用 `BindListenPort` 方法进行处理。下面我们开始讲解这个方法。 280 | 281 | - 首先,生成一个通用的网络地址 sockaddr 对象,类型为 sockaddr_storage,它的长度是 128个字节。 282 | 283 | - 然后,调用 `addrBind.GetSockAddr((struct sockaddr*)&sockaddr, &len)` 方法来设置网络地址 sockaddr。 284 | 285 | `GetSockAddr` 方法内部根据地址是 IPV4 或 IPV6,分别进行处理。 286 | 287 | 如果是 IPV4,则生成 sockaddr_in 地址对象,然后调用 `memset` 把结构体所占内存用0填充,然后调用 `GetInAddr` 方法来设置地址对象的地址字段,最后设置地址类型为 AF_INET 和端口号。 288 | 289 | 如果是 IPV6,则生成 sockaddr_in6 地址对象,然后调用 `memset` 把结构体所占内存用0填充,然后调用 `GetIn6Addr` 方法来设置地址对象的地址字段,最后设置地址类型为 AF_INET6 和端口号。 290 | 291 | - 再然后,调用 `CreateSocket(addrBind)` 方法生成套接字对象。 292 | 293 | 方法处理如下: 294 | 295 | - 首先,生成一个通用的网络地址 sockaddr 对象,类型为 sockaddr_storage,然后,调用 `addrBind.GetSockAddr((struct sockaddr*)&sockaddr, &len)` 方法来设置网络地址 sockaddr。具体分析详见上面。 296 | 297 | - 然后,生成套接字。 298 | 299 | socket(((struct sockaddr*)&sockaddr)->sa_family, SOCK_STREAM, IPPROTO_TCP) 300 | 301 | - 再然后,对套接字进行一些检查和处理,不详述。 302 | 303 | - 套接字生成之后,接下来把套接字绑定到指定的地址上,并监听入站请求。 304 | 305 | if (::bind(hListenSocket, (struct sockaddr*)&sockaddr, len) == SOCKET_ERROR) 306 | { 307 | int nErr = WSAGetLastError(); 308 | if (nErr == WSAEADDRINUSE) 309 | strError = strprintf(_("Unable to bind to %s on this computer. %s is probably already running."), addrBind.ToString(), _(PACKAGE_NAME)); 310 | else 311 | strError = strprintf(_("Unable to bind to %s on this computer (bind returned error %s)"), addrBind.ToString(), NetworkErrorString(nErr)); 312 | LogPrintf("%s\n", strError); 313 | CloseSocket(hListenSocket); 314 | return false; 315 | } 316 | LogPrintf("Bound to %s\n", addrBind.ToString()); 317 | 318 | // Listen for incoming connections 319 | if (listen(hListenSocket, SOMAXCONN) == SOCKET_ERROR) 320 | { 321 | strError = strprintf(_("Error: Listening for incoming connections failed (listen returned error %s)"), NetworkErrorString(WSAGetLastError())); 322 | LogPrintf("%s\n", strError); 323 | CloseSocket(hListenSocket); 324 | return false; 325 | } 326 | 327 | - 最后,进行一些收尾工作。 328 | 329 | 把套接字放入 `vhListenSocket` 集合中。如果地址是可达的,并且不是白名单中的地址,则调用 `AddLocal` 方法,加入本地地址集合中。 330 | 331 | - 处理完地址绑定之后,接下来处理种子节点参数指定集合。 332 | 333 | void CConnman::AddOneShot(const std::string& strDest) 334 | { 335 | LOCK(cs_vOneShots); 336 | vOneShots.push_back(strDest); 337 | } 338 | 339 | 这个方法非常简单,把每个种子节点加入 `vOneShots` 集合。 340 | 341 | - 接下来,从文件数据库中加载地址列表和禁止地址列表。 342 | 343 | { 344 | CAddrDB adb; 345 | if (adb.Read(addrman)) 346 | LogPrintf("Loaded %i addresses from peers.dat %dms\n", addrman.size(), GetTimeMillis() - nStart); 347 | else { 348 | addrman.Clear(); // Addrman can be in an inconsistent state after failure, reset it 349 | LogPrintf("Invalid or missing peers.dat; recreating\n"); 350 | DumpAddresses(); 351 | } 352 | } 353 | 354 | CBanDB bandb; 355 | banmap_t banmap; 356 | if (bandb.Read(banmap)) { 357 | SetBanned(banmap); // thread save setter 358 | SetBannedSetDirty(false); // no need to write down, just read data 359 | SweepBanned(); // sweep out unused entries 360 | 361 | LogPrint(BCLog::NET, "Loaded %d banned node ips/subnets from banlist.dat %dms\n", 362 | banmap.size(), GetTimeMillis() - nStart); 363 | } else { 364 | LogPrintf("Invalid or missing banlist.dat; recreating\n"); 365 | SetBannedSetDirty(true); // force write 366 | DumpBanlist(); 367 | } 368 | 369 | 代码比较简单,一看便知,不作具体展开。 370 | 371 | - **最后,重中之重的线程相关处理终于要到来了**。 372 | 373 | - 首先,生成套接字相关的线程,以便进行网络的接收和发送。处理方法和前面线程的类似,代码如下: 374 | 375 | threadSocketHandler = std::thread(&TraceThread >, "net", std::function(std::bind(&CConnman::ThreadSocketHandler, this))); 376 | 377 | 真正执行的方法是 `ThreadSocketHandler`,这个方法太重要了,我们留在下一课网络处理中细讲。 378 | 379 | - 接下来,处理 DNS 种子节点线程,处理 DNS 种子相关的逻辑。代码如下: 380 | 381 | if (!gArgs.GetBoolArg("-dnsseed", true)) 382 | LogPrintf("DNS seeding disabled\n"); 383 | else 384 | threadDNSAddressSeed = std::thread(&TraceThread >, "dnsseed", std::function(std::bind(&CConnman::ThreadDNSAddressSeed, this))); 385 | 386 | 真正执行的方法是 `ThreadDNSAddressSeed`,这个方法太重要了,我们留在下一课网络处理中细讲。 387 | 388 | - 接下来,处理出站连接。代码如下: 389 | 390 | threadOpenAddedConnections = std::thread(&TraceThread >, "addcon", std::function(std::bind(&CConnman::ThreadOpenAddedConnections, this))); 391 | 392 | 真正执行的方法是 `ThreadOpenAddedConnections`,这个方法太重要了,我们留在下一课网络处理中细讲。 393 | 394 | - 接下来,处理打开连接的线程。代码如下: 395 | 396 | if (connOptions.m_use_addrman_outgoing || !connOptions.m_specified_outgoing.empty()) 397 | threadOpenConnections = std::thread(&TraceThread >, "opencon", std::function(std::bind(&CConnman::ThreadOpenConnections, this, connOptions.m_specified_outgoing))); 398 | 399 | 真正执行的方法是 `ThreadOpenConnections`,这个方法太重要了,我们留在下一课网络处理中细讲。 400 | 401 | - 最最重要的线程--处理消息的线程,隆重登场。 402 | 403 | threadMessageHandler = std::thread(&TraceThread >, "msghand", std::function(std::bind(&CConnman::ThreadMessageHandler, this))); 404 | 405 | 真正执行的方法是 `ThreadMessageHandler`,这个方法太重要了,我们留在下一课网络处理中细讲。 406 | 407 | - 最后,定时把节点地址和禁止列表刷新到数据库文件中。 408 | -------------------------------------------------------------------------------- /start/setup13.md: -------------------------------------------------------------------------------- 1 | # 如何接入比特币网络以及原理分析 2 | 3 | 以下内容为系统启动过程中,每一步骤的详细分析。 4 | 5 | 6 | ### 第13步,结束启动(`src/init.cpp::AppInitMain()`) 7 | 8 | 1. 调用 `SetRPCWarmupFinished()` 方法,设置热身结束。 9 | 10 | 方法内部主要设置 `fRPCInWarmup` 变量为假,表示热身结束。 11 | 12 | 2. 调用钱包接口对象的 `Start` 方法,开始进行钱包相关的处理,并定时刷新钱包数据到数据库中。 13 | 14 | g_wallet_init_interface.Start(scheduler); 15 | 16 | 方法内部调用 `GetWallets` 方法,返回钱包集合 `vpwallets`,调用每个钱包的 `postInitProcess` 方法,进行初始后的处理。主要是把钱包中存在,但是交易池中不存在的交易添加到交易池中。然后,调用调度器定时调用 `MaybeCompactWalletDB` 方法,刷新钱包数据到数据库中。 17 | 18 | 代码如下: 19 | 20 | for (const std::shared_ptr& pwallet : GetWallets()) { 21 | pwallet->postInitProcess(); 22 | } 23 | scheduler.scheduleEvery(MaybeCompactWalletDB, 500); 24 | -------------------------------------------------------------------------------- /start/start.md: -------------------------------------------------------------------------------- 1 | # 如何接入比特币网络以及原理分析 2 | 3 | ## 1、如何接入比特币网络? 4 | 5 | 其实接入比特币网络是非常简单的,我说了你一定不信,启动比特币客户端即可: 6 | 7 | 在命令行终端输入启动命令:`./src/bitcoind -testnet` 8 | 9 | 输入之后会有一个和网络同步数据的过程,你会看到: 10 | 11 | ![数据同步](http://ocie6rxms.bkt.clouddn.com/btc-sync.png) 12 | 13 | ![数据同步](http://ocie6rxms.bkt.clouddn.com/btc-sync2.png) 14 | 15 | 这个过程需要一点时间,同步数据完成后,即接入了比特币网络。 16 | 17 | 18 | ## 2、启动流程鸟瞰 19 | 20 | 虽然说一句命令即搞定,但是,这个背后代码运行的逻辑可就不简单咯~ 21 | 22 | 来,我给大家分析一下 23 | 24 | 当在命令行终端输入启动命令:`./src/bitcoind -testnet` 后,操作系统就会找到这个文件中的 main 函数,开始比特币客户端的启动。 25 | 26 | 对于所有的c++代码,整个程序都是从main函数开始执行的,bitcoind 的main函数位于 `src/bitcoind.cpp`,代码拉到最后就找到了我们的 main 函数。 27 | 28 | main 函数本身没有太多东西,主要是调用3个函数来执行,它们的主要作用是设置环境变量、设置信号处理和启动系统。 29 | 30 | 具体代码如下: 31 | 32 | int main(int argc, char* argv[]) 33 | { 34 | SetupEnvironment(); 35 | noui_connect(); 36 | return (AppInit(argc, argv) ? EXIT_SUCCESS : EXIT_FAILURE); 37 | } 38 | 39 | 代码说明如下: 40 | 41 | 1. `SetupEnvironment` 函数,主要用来设置系统的环境变量,包括:`malloc` 分配内存的行为、Locale、文件路径的本地化设置等。 42 | 43 | 2. `noui_connect` 函数,设置连接到 bitcoind 的信号的处理。 44 | 45 | 3. `AppInit` 函数,进行系统启动。 46 | 47 | 48 | 下面我们重点讲下 `AppInit` 函数的执行 49 | 50 | 1. 调用 `SetupServerArgs` 函数,设置系统可接受的所有命令行参数。然后开始解析命令行传递的各种参数。 51 | 52 | 系统执行的重要一步就是就设置可以接收的参数,解析用户启动时传递的各种参数,`SetupServerArgs` 函数就是完成这个目的。下面来看这个函数的执行流程。 53 | 54 | - 首先,调用 `CreateBaseChainParams` 函数,生成默认的基本参数,包括:使用的数据目录和监听的端口。根据不同的网络类型,主网络使用 8332 端口和指定目录下的当前目录,测试网络使用 18332 端口和指定目录下的 testnet3 子目录,回归测试网络使用 18443 端口和指定目录下的 regtest 子目录。 55 | 56 | const auto defaultBaseParams = CreateBaseChainParams(CBaseChainParams::MAIN); 57 | const auto testnetBaseParams = CreateBaseChainParams(CBaseChainParams::TESTNET); 58 | const auto regtestBaseParams = CreateBaseChainParams(CBaseChainParams::REGTEST); 59 | 60 | - 然后,调用 `CreateChainParams` 函数,生成默认的区块链参数。这个方法也会区分不同的网络。 61 | 62 | const auto defaultChainParams = CreateChainParams(CBaseChainParams::MAIN); 63 | const auto testnetChainParams = CreateChainParams(CBaseChainParams::TESTNET); 64 | const auto regtestChainParams = CreateChainParams(CBaseChainParams::REGTEST); 65 | 66 | 上面3个对象的定义都在 `chainparams.cpp` 文件中。 67 | 68 | - 接下来,生成并初始化隐藏参数 `hidden_args` 集合。 69 | 70 | - -h 71 | 72 | - -help 73 | 74 | - -dbcrashratio 75 | 76 | - -forcecompactdb 77 | 78 | - -allowselfsignedrootcertificates 79 | 80 | - -choosedatadir 81 | 82 | - -lang= 83 | 84 | - -min 85 | 86 | - -resetguisettings 87 | 88 | - -rootcertificates= 89 | 90 | - -splash 91 | 92 | - -uiplatform 93 | 94 | - 再接下来,设置系统可接收的所有参数及他们的帮助信息。 95 | 96 | 除了 `SetupServerArgs` 方法中直接列出的这些之外,还通过下面三种方法设置了一些参数: 97 | 98 | - 通过调用钱包接口(`wallet/init.cpp`)的 `AddWalletOptions` 方法,设置钱包相关的参数; 99 | 100 | - 通过调用 `SetupChainParamsBaseOptions` 方法,设置区块链相关的参数; 101 | 102 | - 通过调用 `AddHiddenArgs` 方法,设置隐藏参数。`hidden_args` 集合除了上面列出的之外,增加了 `-upnp`、`-zmqpubhashblock=
`、`-zmqpubhashtx=
`、`-zmqpubrawblock=
`、`-zmqpubrawtx=
`、`-daemon` 等几个。 103 | 104 | **具体有哪些参数,可以参考本文的第三部分:系统可接受的参数** 105 | 106 | 2. 接下来,检查用户指定命令参数是否正确。 107 | 108 | if (!gArgs.ParseParameters(argc, argv, error)) { 109 | fprintf(stderr, "Error parsing command line arguments: %s\n", error.c_str()); 110 | return false; 111 | } 112 | 113 | 3. 如果传递的是帮助和版本参数,则显示帮助或版本信息,然后退出。 114 | 115 | 4. 检查数据目录(可指定或默认)是否是存在。如果不存在,则打印错误信息,然后退出。 116 | 117 | if (!fs::is_directory(GetDataDir(false))) 118 | { 119 | fprintf(stderr, "Error: Specified data directory \"%s\" does not exist.\n", gArgs.GetArg("-datadir", "").c_str()); 120 | return false; 121 | } 122 | 123 | 在 `GetDataDir` 方法中,根据用户是否在命令行提供 `datadir` 参数来确定使用默认的数据目录还是用户指定的数据目录。 124 | 125 | 5. 读取并解析配置文件,同时检查指定数据目录是否存在。如果任何一个步骤出错,都打印错误信息,然后退出。 126 | 127 | if (!gArgs.ReadConfigFiles(error, true)) { 128 | fprintf(stderr, "Error reading configuration file: %s\n", error.c_str()); 129 | return false; 130 | } 131 | 132 | `ReadConfigFiles` 方法具体处理如下: 133 | 134 | - 首先,调用 `GetArg` 方法,获取配置文件名称,默认为 `bitcoin.conf`。 135 | 136 | - 然后,通过 `GetConfigFile` 方法获取配置文件的绝对路径(方法内部会委托 `AbsPathForConfigVal` 方法进行处理,后者决定根据用户指定的路径或使用默认路径来生成配置文件的绝对路径)。在得到配置文件的绝对路径之后,构造文件输入流,从而读取配置文件 `fs::ifstream stream(GetConfigFile(confPath))`。 137 | 138 | - 在成功构造输入流之后,调用 `ReadConfigStream` 方法开始读取配置文件的内容。 139 | 140 | 方法内部按行读取配置文件,并以键值对的形式保存在 `m_config_args` 集合中。 141 | 142 | 6. 调用 `SelectParams(gArgs.GetChainName())` 函数,生成全局的区块链参数,并设置系统的网络类型。如果有错误,则打印错误,然后退出。 143 | 144 | `gArgs.GetChainName()` 方法会返回当前使用的网络。针对主网络,返回字符串 `main`;测试网络,返回字符串 `test`;回归测试网络,返回字符串 `regtest`。 145 | 146 | `SelectParams` 方法的实现如下所示: 147 | 148 | void SelectParams(const std::string& network) 149 | { 150 | SelectBaseParams(network); 151 | globalChainParams = CreateChainParams(network); 152 | } 153 | 154 | `SelectBaseParams` 方法会根据指定的网络参数生成 `CBaseChainParams` 对象,并保存在 `globalChainBaseParams` 变量中,并在指定 `gArgs` 对象中保存网络类型(`m_network` 属性)。`CBaseChainParams` 对象中仅保存系统的数据目录和运行的端口,所以称之为基本区块链参数对象。 155 | 156 | `CreateChainParams` 方法会根据不同的网络参数生成 `CChainParams` 类的子对象,可能为以下三种:CMainParams、CTestNetParams、CRegTestParams。`CChainParams` 对象包含了区块链对象的所有重要信息,比如:共识规则、部署状态、检查点、创世区块等。 157 | 158 | 7. 检查所有命令行参数,如果有错误,则打印错误,并退出。 159 | 160 | 8. 设置参数 `-server` 默认为真。 161 | 162 | gArgs.SoftSetBoolArg("-server", true); 163 | 164 | bitcoind 守护进程默认 `server` 为真。 165 | 166 | 9. 调用 `InitLogging` 函数,初始化系统所用日志,并打印系统的版本信息。 167 | 168 | 具体代码如下,根据是否指定 `debuglogfile`、`printtoconsole` 等确定日志打印到文件或是控制台。 169 | 170 | void InitLogging() 171 | { 172 | g_logger->m_print_to_file = !gArgs.IsArgNegated("-debuglogfile"); 173 | g_logger->m_file_path = AbsPathForConfigVal(gArgs.GetArg("-debuglogfile", DEFAULT_DEBUGLOGFILE)); 174 | 175 | LogPrintf("\n\n\n\n\n"); 176 | 177 | g_logger->m_print_to_console = gArgs.GetBoolArg("-printtoconsole", !gArgs.GetBoolArg("-daemon", false)); 178 | g_logger->m_log_timestamps = gArgs.GetBoolArg("-logtimestamps", DEFAULT_LOGTIMESTAMPS); 179 | g_logger->m_log_time_micros = gArgs.GetBoolArg("-logtimemicros", DEFAULT_LOGTIMEMICROS); 180 | 181 | fLogIPs = gArgs.GetBoolArg("-logips", DEFAULT_LOGIPS); 182 | 183 | std::string version_string = FormatFullVersion(); 184 | 185 | LogPrintf(PACKAGE_NAME " version %s\n", version_string); 186 | } 187 | 188 | 10. 调用 `InitParameterInteraction` 函数,根据参数间的关系,检查所有的交互参数。 189 | 190 | 11. 调用 `AppInitBasicSetup` 函数,进行基本的设置。如果有错误,则打印错误,然后退出。 191 | 192 | 经过前面漫长的检查与设置,终于开始了应用基本的设置。 193 | 194 | 具体讲解参见启动过程的第一步。 195 | 196 | 12. 调用 `AppInitParameterInteraction` 函数,处理参数的交互设置。 197 | 198 | 具体讲解参见启动过程的第二、三步。 199 | 200 | 13. 调用 `AppInitSanityChecks` 函数,处理底层加密函数相关内容。 201 | 202 | 具体讲解参见启动过程的第四步。 203 | 204 | 14. 调用 `AppInitLockDataDirectory` 函数,检查并锁定数据目录。 205 | 206 | 方法内部调用 `LockDataDirectory` 方法,锁定数据目录。如果锁定失败,则返回假,否则,返回真。 207 | 208 | 锁定数据目录的目的是为了防止多个客户端同时启动。 209 | 210 | 15. 调用 `AppInitMain` 函数,比特币主要的启动过程。 211 | 212 | 具体讲解参见启动过程的第四a 到十三步。 213 | 214 | 16. 如果应用初始化主函数出错,则调用 `Interrupt` 函数进行中止,否则调用 `WaitForShutdown` 函数等待系统结束。 215 | 216 | `WaitForShutdown` 函数是一个无限循环函数。 217 | 218 | 219 | ## 3、系统可接受的参数 220 | 221 | ### 常用参数 222 | 223 | - ?,显示帮助信息; 224 | 225 | - -version,打印版本信息,并退出系统。 226 | 227 | - -assumevalid=hex,如果指定的区块存在区块链中,假定它及其祖先有效并可能跳过其脚本验证。 228 | 229 | - -blocksdir=dir,指定区块链存放的目录。 230 | 231 | - -blocknotify=cmd,指定当主链上的区块改变时执行的命令。 232 | 233 | - -conf=file,指定配置文件的目录,相对于下面指定的数据目录。 234 | 235 | - -datadir=dir,指定数据目录。 236 | 237 | - -dbcache=n,设置数据库缓存大小。 238 | 239 | - -debuglogfile=file,设置调试文件的位置。 240 | 241 | - -feefilter,告诉其他节点通过最小交易费用过滤发送给我们的库存消息。 242 | 243 | - -loadblock=file,在启动时,从外部 blk000??.dat 文件导入区块。 244 | 245 | - -maxmempool=n,指定交易池的最大内存数,单位为兆字节。 246 | 247 | - -maxorphantx=n,指定内存中最大的孤儿交易数量。 248 | 249 | - -mempoolexpiry=n,指定交易池中不跟踪超过指定时间(小时)的交易。 250 | 251 | - -par=n,指定脚本签名的线程数量。 252 | 253 | - -persistmempool,指定是否持久化交易池中的交易,启动时恢复加载。 254 | 255 | - -pid=file,指定进程文件。 256 | 257 | - -prune=n,通过启用旧区块的修剪(删除)来降低存储要求。 这允许调用 `pruneblockchain` RPC 来删除特定块,并且如果提供目标大小,则启用对旧块的自动修剪。 此模式与 `-txindex` 和 `-rescan` 不兼容。 258 | 259 | - -reindex,根据硬盘上的 `blk*.dat` 文件重建区块链状态和区块的索引。 260 | 261 | - -reindex-chainstate,根据当前区块的索引重建区块链的状态。 262 | 263 | - -txindex,维护所有交易的索引,被 `getrawtransaction` RPC 命令调用。 264 | 265 | - -addnode=ip,添加一个节点,并连接它,并保持连接。 266 | 267 | - -banscore=n,断开行为不端的同伴的门槛。 268 | 269 | - -bantime=n,不诚实节点重新连接需要的秒数。 270 | 271 | - -bind=addr,绑定到指定的IP,并始终监听它。 272 | 273 | - -connect=ip,仅仅只连接到指定的节点,如果不是ip而是0,则表示禁止自动连接。 274 | 275 | - -discover,是否发现自己的IP地址。 276 | 277 | - -dns,对于 `-addnode`、`-seednode`、`-connect` 总是使用 DNS 查找。 278 | 279 | - -dnsseed,指定如果已有地址比较少,则进行 DNS 查找来获取对等节点。 280 | 281 | - -enablebip61,允许发送 BIP61 定义的拒绝消息。 282 | 283 | - -externalip=ip,指定自身的外部 IP 地址。 284 | 285 | - -forcednsseed,总是通过 DNS 查找来获取对等节点的地址。 286 | 287 | - -listen,接收外部对等节点的连接。 288 | 289 | - -listenonion,自动创建 Tor 隐藏服务。 290 | 291 | - -maxconnections=n,维护到别的节点的最大连接数。 292 | 293 | - -maxreceivebuffer=n,每个对等节点的最大接收缓存。 294 | 295 | - -maxsendbuffer=n,每个对等节点的最大发送缓存。 296 | 297 | - -onion=ip:port,设置 SOCKS5 代理。 298 | 299 | - -peerbloomfilters,支持布隆过滤器过滤区块和交易。 300 | 301 | - -permitbaremultisig,中继非 P2SH 多重签名。 302 | 303 | - -port=port,指定默认的监听端口。 304 | 305 | - -proxy=ip:port,通过 SOCKS5 代理进行连接。 306 | 307 | - -proxyrandomize,随机化每个代理连接的凭据。 从而使Tor流进行隔离。 308 | 309 | - -seednode=ip,指定一个节点来检索其他的节点,随后就从这个接点进行断开。 310 | 311 | - -torcontrol=ip:port,在 onion 启用的情况下,指定 Tor 控制器使用的端口。 312 | 313 | - -torpassword=pass,Tor 控制器的密码。 314 | 315 | - -whitebind=addr,本节点绑定到这个地址上,白名单中的节点通过这个地址连接到节点。 316 | 317 | - -whitelist=IP address or network,当收到连接请求的时候,如果节点在这个白名单中,就直接中继而不用检查。 318 | 319 | - -checkblocks=n,在启动时要检查多少个区块。 320 | 321 | - -checklevel=n,`checkblocks` 验证区块的程度。 322 | 323 | - -checkblockindex,进行完整的一致性检查,包括:mapBlockIndex、setBlockIndexCandidates、chainActive、mapBlocksUnlinked 等。 324 | 325 | - -checkmempool=n,每多少个交易进行检验。 326 | 327 | - -checkpoints,提供检查点,对已知链的历史不进行检验。 328 | 329 | - -deprecatedrpc=method,不赞成使用的 RPC 方法。 330 | 331 | - -limitancestorcount=n,如果交易池中的祖先交易达到或超过指定的值时,不再接收交易。 332 | 333 | - -limitancestorsize=n,如果交易池中的祖先交易大小达到或超过指定的值时,不再接收交易。 334 | 335 | - -limitdescendantcount=n,如果交易池中祖先交易的后代已经达到或超过指定的值时,不再接收交易。 336 | 337 | - -blockmaxweight=n,设置 BIP141 区块的最大 weight。 338 | 339 | - -blockmintxfee=amt,设置包含在创建区块的交易最小费用。 340 | 341 | - -blockversion=n,重写区块版本号,以测试分叉方案。 342 | 343 | - -rest,允许 REST 请求。 344 | 345 | - -rpcallowip=ip,设置允许 JSON-RPC 连接的地址/来源。 346 | 347 | - -rpcauth=userpw,JSON-RPC 连接的用户名和密码哈希。 348 | 349 | - -rpcbind=addr:port,绑定到指定的地址来监听 JSON-RPC 连接。 350 | 351 | - -rpccookiefile=loc,进行验证的的 cookie 位置。 352 | 353 | - -rpcuser=user,JSON-RPC 连接的用户名。 354 | 355 | - -rpcpassword=pw,JSON-RPC 连接的用户密码。 356 | 357 | - -rpcport=port,JSON-RPC连接监听的端口。 358 | 359 | - -rpcservertimeout=n,HTTP 请求的超时时间。 360 | 361 | - -rpcthreads=n,设置服务 RPC 调用的线程数量。 362 | 363 | - -rpcworkqueue=n,设置服务 RPC 调用的工作队列深度。 364 | 365 | - -server,接受命令行和 JSON-RPC 命令。 366 | 367 | - -debug=category,输入调试信息。如果输出类别(category)不指定默认输出所有调试信息。类别包括:addrman、alert、bench、cmpctblock、coindb、db、http、libevent、lock、mempool、mempoolrej、net、proxy、prune、rand、reindex、rpc、selectcoins、tor、zmq、qt 等。 368 | 369 | ### 钱包相关的参数 370 | 371 | 钱包相关的参数通过调用 `AddWalletOptions` 方法,被加入 `gArgs` 中。钱包相关的参数有如下这些: 372 | 373 | - -addresstype 374 | 375 | - -avoidpartialspends 376 | 377 | - -changetype 378 | 379 | - -disablewallet 380 | 381 | - -discardfee= 382 | 383 | - -fallbackfee= 384 | 385 | 当费用估算数据不足时,将使用的费率。 386 | 387 | - -keypool= 388 | 389 | - -mintxfee= 390 | 391 | - -paytxfee= 392 | 393 | 交易费用。 394 | 395 | - -rescan 396 | 397 | 在启动时,扫描区块链 398 | 399 | - -salvagewallet 400 | 401 | - -spendzeroconfchange 402 | 403 | 发送交易时,是否可以花费未确认的变更(交易)。 404 | 405 | - -txconfirmtarget= 406 | 407 | 如果没有设置交易费用,则应包含足够的费用,以便在平均 n 个区块内打包交易。 408 | 409 | - -upgradewallet 410 | 411 | - -wallet= 412 | 413 | - -walletbroadcast 414 | 415 | - -walletdir= 416 | 417 | - -walletnotify= 418 | 419 | - -walletrbf 420 | 421 | 发送交易时,是否启用 full-RBF opt-in。 422 | 423 | - -zapwallettxes= 424 | 425 | - -dblogsize= 426 | 427 | - -flushwallet 428 | 429 | - -privdb 430 | 431 | - -walletrejectlongchains 432 | 433 | 434 | ## 4、三种网络的参数 435 | 436 | 437 | ### 4.1、主网络 438 | 439 | 主网络由类 `CMainParams` 表示。在构造函数中,进行如下的设置: 440 | 441 | 1. 网络ID `strNetworkID` 为 `main`; 442 | 443 | 2. 共识参数(`Consensus::Params`)的各个值: 444 | 445 | - 每隔多少个块(`nSubsidyHalvingInterval`)后续比特币的奖励会减半,值为 210000。 446 | 447 | 根据创世区块奖励的数量(50),根据等比数列求和公式:$$ 50 * (1 / (1 - 0.5)) * 210000 $$,可计算货币总量为 2100W个比特币。 448 | 449 | - BIP16Exception 450 | 451 | `0x00000000000002dc756eebf4f49723ed8d30cc28a5f108eb94b1ba88ac4f9c22` 452 | 453 | - BIP34 激活高度(`BIP34Height`)为 227931。 454 | 455 | - BIP34 激活哈希`BIP34Hash` 456 | 457 | `0x000000000000024b89b42a942fe0d9fea3bb44ab7bd1b19115dd6a759c0808b8` 458 | 459 | - BIP65 激活高度 `BIP65Height` 460 | 461 | 388381 462 | 463 | - BIP66 激活高度 `BIP66Height` 464 | 465 | 363725 466 | 467 | - 工作量限制`powLimit` 468 | 469 | `00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff`。 470 | 471 | - 难度改变的周期 `nPowTargetTimespan` 472 | 473 | 2周,`14 * 24 * 60 * 60` 秒 474 | 475 | - 平均出块时间 `nPowTargetSpacing` 476 | 477 | 10分钟,`10 * 60`。 478 | 479 | - 改变共识需要的区块数 `nRuleChangeActivationThreshold` 480 | 481 | 1916 个区块,即 2016 的 95% 482 | 483 | - 矿工确认窗口 `nMinerConfirmationWindow` 484 | 485 | 2016,等于难度改变周期除以平均出块时间。 486 | 487 | - 区块链相关的部署状态,包括: 488 | 489 | - 测试相关的 `DEPLOYMENT_TESTDUMMY` 490 | 491 | - CSV 软分叉相关的,涉及到 BIP68、BIP112、BIP113 492 | 493 | - 隔离见证相关的,涉及到 BIP141、BIP143、BIP147。 494 | 495 | - 最佳区块链的最小工作量 496 | 497 | `0x0000000000000000000000000000000000000000028822fef1c230963535a90d` 498 | 499 | - 创世区块的哈希 500 | 501 | 值为 `CreateGenesisBlock` 方法生成的创世区块的哈希。 502 | 503 | 3. 设置默认端口 `nDefaultPort` 504 | 505 | 8333 506 | 507 | 4. 达到多少个区块之后进行区块修剪 `nPruneAfterHeight` 508 | 509 | 100000 510 | 511 | 5. 设置 DNS 种子节点 `vSeeds` 集合包含的 DNS 种子 512 | 513 | 通过解析 DNS 种子节点,比特币节点启动时可以找到更多的对等节点来进行连接。 514 | 515 | 6. `base58Prefixes` 集合中各个前缀 516 | 517 | 包括以下几种: 518 | 519 | - 公钥地址 520 | 521 | 0 522 | 523 | - 脚本地址 524 | 525 | 5 526 | 527 | - 私钥前缀 528 | 529 | 128 530 | 531 | - 扩展公钥 532 | 533 | 4、136、178、30 534 | 535 | - 扩展私钥 536 | 537 | 4、136、173、228 538 | 539 | 7. bech32_hrp 540 | 541 | bc 542 | 543 | 8. vFixedSeeds 544 | 545 | 固定的种子节点 546 | 547 | 9. fDefaultConsistencyChecks 548 | 549 | 假 550 | 551 | 10. fRequireStandard 552 | 553 | 真 554 | 555 | 11. fMineBlocksOnDemand 556 | 557 | 不进行挖矿 558 | 559 | 12. checkpointData 560 | 561 | 13. chainTxData 562 | 563 | 14. m_fallback_fee_enabled 564 | 565 | 禁止回退费用 566 | 567 | 568 | ### 4.2、测试网络 569 | 570 | 1. `base58Prefixes` 集合中各个前缀 571 | 572 | 包括以下几种: 573 | 574 | - 公钥地址 575 | 576 | 111 577 | 578 | - 脚本地址 579 | 580 | 196 581 | 582 | - 私钥前缀 583 | 584 | 239 585 | 586 | - 扩展公钥 587 | 588 | 4、53、135、207 589 | 590 | - 扩展私钥 591 | 592 | 4、53、131、148 593 | 594 | ### 4.3、回归测试网络 595 | 596 | 1. `base58Prefixes` 集合中各个前缀 597 | 598 | 包括以下几种: 599 | 600 | - 公钥地址 601 | 602 | 111 603 | 604 | - 脚本地址 605 | 606 | 196 607 | 608 | - 私钥前缀 609 | 610 | 239 611 | 612 | - 扩展公钥 613 | 614 | 4、53、135、207 615 | 616 | - 扩展私钥 617 | 618 | 4、53、131、148 619 | -------------------------------------------------------------------------------- /wallet/GenerateNewSeed.md: -------------------------------------------------------------------------------- 1 | 2 | # 写在前面 3 | 4 | 创建钱包和交易是比特币最重要的两方面,涉及到很多很多的内容,远非一篇文章能概括的完。上一篇从整体上讲解了钱包的创建流程,虽然已经很详细了,但还是漏掉了几个重要的方法,从本篇开始我们讲解这几个特别重要的方法。对钱包感兴趣的朋友一定不要错误。 5 | 6 | # GenerateNewSeed 生成新的私钥/公钥 7 | 8 | 这个方法主要用来生成私钥/公钥,在生成公钥后,调用钱包对象的 `SetHDSeed` 方法,根据公钥生成一个 `CHDChain` 对象,把公钥经过 SHA256、RIPEMD160 两次哈希返回后的 20个字节 180 位字符串作为它的 `seed_id` 属性,从而以后可以衍生更的扩展公钥。 9 | 10 | void CWallet::SetHDSeed(const CPubKey& seed) 11 | { 12 | LOCK(cs_wallet); 13 | CHDChain newHdChain; 14 | newHdChain.nVersion = CanSupportFeature(FEATURE_HD_SPLIT) ? CHDChain::VERSION_HD_CHAIN_SPLIT : CHDChain::VERSION_HD_BASE; 15 | newHdChain.seed_id = seed.GetID(); 16 | SetHDChain(newHdChain, false); 17 | } 18 | 19 | 20 | 在创建钱包的过程中,本方法及下面的 `TopUpKeyPool` 方法,可能会被调用两次,一次在第一次创建钱包时,另一次在用户明确升级,且钱包支持 HD,但 HD 没启用的情况下。 21 | 22 | 下面,我们开始正式讲解这个方法。 23 | 24 | 1. 首先,生成并调用私钥的 `MakeNewKey` 来初始化私钥。 25 | 26 | CKey key; 27 | key.MakeNewKey(true); 28 | 29 | 2. 然后,调用 `DeriveNewSeed` 方法,生成并返回私钥对应的公钥。 30 | 31 | return DeriveNewSeed(key); 32 | 33 | 我们先来看 `MakeNewKey` 这个方法。这个方法比较简单,但是非常重要,因为正是这个方法,生成了真正意义上的私钥。 34 | 35 | void CKey::MakeNewKey(bool fCompressedIn) { 36 | do { 37 | GetStrongRandBytes(keydata.data(), keydata.size()); 38 | } while (!Check(keydata.data())); 39 | fValid = true; 40 | fCompressed = fCompressedIn; 41 | } 42 | 43 | `GetStrongRandBytes` 方法,正是生成私钥的过程。这个方法的执行逻辑如下: 44 | 45 | 1. 生成一个 SHA512对象和一个无符号字符数组。 46 | 47 | CSHA512 hasher; 48 | unsigned char buf[64]; 49 | 50 | 2. 首先,调用 `RandAddSeedPerfmon` 方法,通过 OpenSSL's RNG 生成随机数,并进行 SHA512 哈希。 51 | 52 | RandAddSeedPerfmon(); 53 | GetRandBytes(buf, 32); 54 | hasher.Write(buf, 32); 55 | 56 | 3. 然后,调用 `GetOSRand` 方法,通过 OS RNG 生成随机数,并进行 SHA512 哈希。 57 | 58 | GetOSRand(buf); 59 | hasher.Write(buf, 32); 60 | 61 | 4. 最后,如果 HW 可用,通过 HW 生成随机数,并进行 SHA512 哈希。 62 | 63 | if (GetHWRand(buf)) { 64 | hasher.Write(buf, 32); 65 | } 66 | 67 | 5. 接下来,合并并更新状态。 68 | 69 | { 70 | WAIT_LOCK(cs_rng_state, lock); 71 | hasher.Write(rng_state, sizeof(rng_state)); 72 | hasher.Write((const unsigned char*)&rng_counter, sizeof(rng_counter)); 73 | ++rng_counter; 74 | hasher.Finalize(buf); 75 | memcpy(rng_state, buf + 32, 32); 76 | } 77 | 78 | 6. 然后,把 `buf` 数组中的内容拷贝到输出参数中,并清空数组中的内容。 79 | 80 | memcpy(out, buf, num); 81 | memory_cleanse(buf, 64); 82 | 83 | `Check` 方法内部通过调用 `secp256k1_ec_seckey_verify` 方法,检查私钥的数据是否满足要求,后者正是椭圆曲线算法相关的。 84 | 85 | 接下来,我们来看 `DeriveNewSeed` 方法是如何生成公钥的。方法执行逻辑如下: 86 | 87 | 1. 生成当前时间,并用当前时间初始化蜜钥元数据。 88 | 89 | int64_t nCreationTime = GetTime(); 90 | CKeyMetadata metadata(nCreationTime); 91 | 92 | 2. 调用私钥的 `GetPubKey` 方法,生成公钥。 93 | 94 | 方法内部正是通过椭圆曲线算法来生成私钥对应的公钥。代码如下,有兴趣的读者可以自行研究。 95 | 96 | assert(fValid); 97 | secp256k1_pubkey pubkey; 98 | size_t clen = CPubKey::PUBLIC_KEY_SIZE; 99 | CPubKey result; 100 | int ret = secp256k1_ec_pubkey_create(secp256k1_context_sign, &pubkey, begin()); 101 | assert(ret); 102 | secp256k1_ec_pubkey_serialize(secp256k1_context_sign, (unsigned char*)result.begin(), &clen, &pubkey, fCompressed ? SECP256K1_EC_COMPRESSED : SECP256K1_EC_UNCOMPRESSED); 103 | assert(result.size() == clen); 104 | assert(result.IsValid()); 105 | return result; 106 | 107 | 3. 执行 `assert(key.VerifyPubKey(seed));` 方法,验证公钥确实有私钥相匹配。 108 | 109 | 4. 设置蜜钥元数据的两个属性。 110 | 111 | metadata.hdKeypath = "s"; 112 | metadata.hd_seed_id = seed.GetID(); 113 | 114 | `GetID` 方法,用公钥的数据经过 SHA256、RIPEMD160 两次哈希后生成的字符串来生成一个 `CKeyID` 对象。 115 | 116 | 5. 把密钥元数据保存在 `mapKeyMetadata` 集合中。 117 | 118 | mapKeyMetadata[seed.GetID()] = metadata; 119 | 120 | 6. 调用 `AddKeyPubKey` 方法,把私钥/公钥及元数据保存到数据库。如果出错,抛出一个异常。 121 | 122 | if (!AddKeyPubKey(key, seed)) 123 | throw std::runtime_error(std::string(__func__) + ": AddKeyPubKey failed"); 124 | 125 | `AddKeyPubKey` 方法生成一个访问数据库的对象,然后调用钱包对象的 `AddKeyPubKeyWithDB` 方法来保存密钥数据。后者的执行流程如下: 126 | 127 | - 如果变量 `encrypted_batch` 为空,那么设置变量 `needsDB` 为真。如果后者为真,那么设置 `encrypted_batch` 属性为参数指定的对象。 128 | 129 | bool needsDB = !encrypted_batch; 130 | if (needsDB) { 131 | encrypted_batch = &batch; 132 | } 133 | 134 | - 调用 `CCryptoKeyStore::AddKeyPubKey` 方法,保存私钥和公钥。如果不成功,进一步,如果变量 `needsDB` 为真,则设置变量 `encrypted_batch` 为假。 135 | 136 | if (!CCryptoKeyStore::AddKeyPubKey(secret, pubkey)) { 137 | if (needsDB) encrypted_batch = nullptr; 138 | return false; 139 | } 140 | 141 | `CCryptoKeyStore::AddKeyPubKey` 方法内部执行逻辑如下: 142 | 143 | - 如果没有加密,那么调用 `CBasicKeyStore::AddKeyPubKey` 来保存私钥和公钥,然后返回。 144 | 145 | if (!IsCrypted()) { 146 | return CBasicKeyStore::AddKeyPubKey(key, pubkey); 147 | } 148 | 149 | `IsCrypted` 方法,初始化时是没有加密的,只有在执行加锁、解锁、添加加密密钥的情况下才会是加密的。当前情景是没有加密的,所以执行基类的方法来保存私钥和公钥。 150 | 151 | `CBasicKeyStore::AddKeyPubKey` 方法,首先把私钥保存在 `mapKeys` 集合中,然后调用 `ImplicitlyLearnRelatedKeyScripts` 方法来处理脚本,因为公钥默认是压缩的,所以在方法内会生成脚本,并把脚本保存在 `mapScripts` 集合中。 152 | 153 | bool CBasicKeyStore::AddKeyPubKey(const CKey& key, const CPubKey &pubkey) 154 | { 155 | LOCK(cs_KeyStore); 156 | mapKeys[pubkey.GetID()] = key; 157 | ImplicitlyLearnRelatedKeyScripts(pubkey); 158 | return true; 159 | } 160 | 161 | - 如果是锁定的,那么返回假。 162 | 163 | if (IsLocked()) { 164 | return false; 165 | } 166 | 167 | `IsLocked` 方法,首先调用 `IsCrypted` 方法,确定是否是加密的,如果不是加密的,则直接返回假;否则,把 `vMasterKey` 集合清空。 168 | 169 | bool CCryptoKeyStore::IsLocked() const 170 | { 171 | if (!IsCrypted()) { 172 | return false; 173 | } 174 | LOCK(cs_KeyStore); 175 | return vMasterKey.empty(); 176 | } 177 | 178 | - 接下来,生成加密私钥。 179 | 180 | std::vector vchCryptedSecret; 181 | CKeyingMaterial vchSecret(key.begin(), key.end()); 182 | if (!EncryptSecret(vMasterKey, vchSecret, pubkey.GetHash(), vchCryptedSecret)) { 183 | return false; 184 | } 185 | 186 | - 最后,调用 `AddCryptedKey` 方法,保存加密私钥,并返回 187 | 188 | if (!AddCryptedKey(pubkey, vchCryptedSecret)) { 189 | return false; 190 | } 191 | return true; 192 | 193 | `AddCryptedKey` 方法,首先确定是否是加密的,如果没有加密则返回假。接下来,把私钥保存在 `mapCryptedKeys` 集合中,然后调用 `ImplicitlyLearnRelatedKeyScripts` 方法来处理脚本,因为公钥默认是压缩的,所以在方法内会生成脚本,并把脚本保存在 `mapScripts` 集合中。 194 | 195 | bool CCryptoKeyStore::AddCryptedKey(const CPubKey &vchPubKey, const std::vector &vchCryptedSecret) 196 | { 197 | LOCK(cs_KeyStore); 198 | if (!SetCrypted()) { 199 | return false; 200 | } 201 | mapCryptedKeys[vchPubKey.GetID()] = make_pair(vchPubKey, vchCryptedSecret); 202 | ImplicitlyLearnRelatedKeyScripts(vchPubKey); 203 | return true; 204 | } 205 | 206 | - 如果变量 `needsDB` 为真,则设置变量 `encrypted_batch` 为空指针。 207 | 208 | - 调用 `GetScriptForDestination` 方法,获得公钥对应的脚本。 209 | 210 | CScript script; 211 | script = GetScriptForDestination(pubkey.GetID()); 212 | 213 | - 调用 `HaveWatchOnly` 方法,检查 `setWatchOnly` 集合中是否有这个脚本。如果有,则调用 `RemoveWatchOnly` 方法,清除公钥对应的脚本。 214 | 215 | if (HaveWatchOnly(script)) { 216 | RemoveWatchOnly(script); 217 | } 218 | 219 | `RemoveWatchOnly` 方法执行逻辑如下: 220 | 221 | - 调用 `CCryptoKeyStore::RemoveWatchOnly` 方法来移除脚本。方法内部执行逻辑如下:从 `setWatchOnly` 集合中移除对应的脚本;然后,调用 `ExtractPubKey` 方法,通过脚本来解析出公钥,如果可以得到公钥,则从 `mapWatchKeys` 集合中移除对应的公钥;最后,返回真。 222 | 223 | - 调用数据库访问对象的 `EraseWatchOnly` 方法,从数据库中移除 `watchmeta` 和 `watchs` 对应的数据。 224 | 225 | - 返回真。 226 | 227 | - 调用 `GetScriptForRawPubKey` 方法,从原始公钥获得脚本。 228 | 229 | script = GetScriptForRawPubKey(pubkey); 230 | 231 | - 调用 `HaveWatchOnly` 方法,检查 `setWatchOnly` 集合中是否有这个脚本。如果有,则调用 `RemoveWatchOnly` 方法,清除公钥对应的脚本。 232 | 233 | 方法执行逻辑如上所述,这里不讲。 234 | 235 | - 如果不是加密的,则调用数据库的访问对象的 `WriteKey` 方法,把私钥和公钥及密钥元数据写入数据库。 236 | 237 | 在这个方法中,首先,以 `keymeta` 为键,保存对应的元数据;然后,把公钥/私钥保存到向量中,以 `key` 为键,保存对应的公钥/私钥。 238 | 239 | - 最后,返回真。 240 | 241 | # 后记 242 | 243 | 由于本人水平所限,文中错误在所难免,欢迎您踊跃指出错误,在下感激不尽。我的微信联系方式:joepeak。 244 | 245 | 原创不易,尤其寒冬,欢迎赞助我一杯咖啡。 246 | 247 |
248 |
249 | drawing 250 |

比特币

251 |
252 |
253 | drawing 254 |

微信

255 |
256 |
257 | drawing 258 |

支付宝

259 |
260 |
261 | -------------------------------------------------------------------------------- /wallet/LoadWallet.md: -------------------------------------------------------------------------------- 1 | # 写在前面 2 | 3 | 创建钱包和交易是比特币最重要的两方面,涉及到很多很多的内容,远非一篇文章能概括的完。上一篇从整体上讲解了钱包的创建流程,虽然已经很详细了,但还是漏掉了几个重要的方法,从本篇开始我们讲解这几个特别重要的方法。对钱包感兴趣的朋友一定不要错误。 4 | 5 | # WalletBatch::LoadWallet 加载钱包 6 | 7 | 本方法,从数据库中加载钱包的相关数据。方法的执行逻辑如下: 8 | 9 | 1. 首先,初始化几个变量。 10 | 11 | CWalletScanState wss; 12 | bool fNoncriticalErrors = false; 13 | DBErrors result = DBErrors::LOAD_OK; 14 | 15 | 2. 从数据库中读取钱包的最小版本。如果最小版本大于当前的最新版本,则返回数据库异常;否则,调用钱包对象的 `LoadMinVersion` 方法,设置钱包的版本相关属性,`nWalletVersion` 属性为数据库读取取的 `nMinVersion`,`nWalletMaxVersion` 属性为 `nWalletMaxVersion` 与这个值的较大者。 16 | 17 | int nMinVersion = 0; 18 | if (m_batch.Read((std::string)"minversion", nMinVersion)) 19 | { 20 | if (nMinVersion > FEATURE_LATEST) 21 | return DBErrors::TOO_NEW; 22 | pwallet->LoadMinVersion(nMinVersion); 23 | } 24 | 25 | 3. 获取数据库游标,如果出错,则返回数据库游标错误。 26 | 27 | Dbc* pcursor = m_batch.GetCursor(); 28 | if (!pcursor) 29 | { 30 | pwallet->WalletLogPrintf("Error getting wallet database cursor\n"); 31 | return DBErrors::CORRUPT; 32 | } 33 | 34 | 4. 进放 ` while (true){ ... }` 循环读取数据库中的所有数据。具体处理如下: 35 | 36 | - 调用 `ReadAtCursor` 方法从游标中读取对应的数据。如果没有读取到数据,则退出循环。如果出现错误,则调用钱包对象的 `WalletLogPrintf` 方法打印,然后返回数据库错误。 37 | 38 | CDataStream ssKey(SER_DISK, CLIENT_VERSION); 39 | CDataStream ssValue(SER_DISK, CLIENT_VERSION); 40 | int ret = m_batch.ReadAtCursor(pcursor, ssKey, ssValue); 41 | if (ret == DB_NOTFOUND) 42 | break; 43 | else if (ret != 0) 44 | { 45 | pwallet->WalletLogPrintf("Error reading next record from wallet database\n"); 46 | return DBErrors::CORRUPT; 47 | } 48 | 49 | - 调用 `ReadKeyValue` 方法,读取游标中当前的内容。如果读取出错,则根据错误进行相应的处理,具体不细说。如果读取数据成功,其内部根据读取到的数据进行具体处理如下: 50 | 51 | - 如果当前的的 Key 是 `name`,那么从流中读取对应的地址到变量 `strAddress`中,调用 `DecodeDestination` 方法解码这个地址,然后设置钱包对象 `mapAddressBook` 集合对应的 `CAddressBookData` 对象的 `name` 属性为流中读取到的名字值。 52 | 53 | if (strType == "name") 54 | { 55 | std::string strAddress; 56 | ssKey >> strAddress; 57 | ssValue >> pwallet->mapAddressBook[DecodeDestination(strAddress)].name; 58 | } 59 | 60 | - 否则,如果前的 Key 是 `purpose` ,那么从流中读取对应的地址到变量 `strAddress`中,调用 `DecodeDestination` 方法解码这个地址,然后设置钱包对象 `mapAddressBook` 集合对应的 `CAddressBookData` 对象的 `purpose` 属性为流中读取到的名字值。 61 | 62 | else if (strType == "purpose") 63 | { 64 | std::string strAddress; 65 | ssKey >> strAddress; 66 | ssValue >> pwallet->mapAddressBook[DecodeDestination(strAddress)].purpose; 67 | } 68 | 69 | - 否则,如果当前的 Key 是 `tx` ,即交易,那么处理如下。 70 | 71 | 从流中读取交易哈希和钱包交易对象,然后调用 `CheckTransaction` 方法,检查读取的钱包交易对象,如果出错,则返回假。 72 | 73 | uint256 hash; 74 | ssKey >> hash; 75 | CWalletTx wtx(nullptr /* pwallet */, MakeTransactionRef()); 76 | ssValue >> wtx; 77 | CValidationState state; 78 | if (!(CheckTransaction(*wtx.tx, state) && (wtx.GetHash() == hash) && state.IsValid())) 79 | return false; 80 | 81 | 如果钱包交易对象的 `fTimeReceivedIsTxTime` 属性大于等于 31404,且小于等于 31703,因为撤销序列化在 31600 中进行了变更,所以进行下面的序列化处理。 82 | 83 | if (31404 <= wtx.fTimeReceivedIsTxTime && wtx.fTimeReceivedIsTxTime <= 31703) 84 | { 85 | if (!ssValue.empty()) 86 | { 87 | char fTmp; 88 | char fUnused; 89 | std::string unused_string; 90 | ssValue >> fTmp >> fUnused >> unused_string; 91 | strErr = strprintf("LoadWallet() upgrading tx ver=%d %d %s", wtx.fTimeReceivedIsTxTime, fTmp, hash.ToString()); 92 | wtx.fTimeReceivedIsTxTime = fTmp; 93 | } 94 | else 95 | { 96 | strErr = strprintf("LoadWallet() repairing tx ver=%d %s", wtx.fTimeReceivedIsTxTime, hash.ToString()); 97 | wtx.fTimeReceivedIsTxTime = 0; 98 | } 99 | wss.vWalletUpgrade.push_back(hash); 100 | } 101 | 102 | 如果钱包交易对象的 `nOrderPos` 为-1,那么设置钱包扫描状态对象的 `fAnyUnordered` 为真。 103 | 104 | if (wtx.nOrderPos == -1) 105 | wss.fAnyUnordered = true; 106 | 107 | 调用钱包对象的 `LoadToWallet`,加载数据库中读到的钱包交易对象到钱包中。 108 | 109 | - 否则,如果当前的 Key 是 `watchs` ,那么从流中读取对应的序列化脚本,并读取其对应的值。如果其值等于1,那么调用钱包对象的 `LoadWatchOnly` 方法,加载一个只读的脚本。 110 | 111 | wss.nWatchKeys++; 112 | CScript script; 113 | ssKey >> script; 114 | char fYes; 115 | ssValue >> fYes; 116 | if (fYes == '1') 117 | pwallet->LoadWatchOnly(script); 118 | 119 | - 否则,如果当前的 Key 是 `key` 或者 `wkey`,那么处理如下。 120 | 121 | 从流中读取对应的公钥,如果 Key 是 `key`,那么从流中读取出对应的私钥,否则从流中读取对应的钱包私钥,从中取得对应的私钥。 122 | 123 | CPubKey vchPubKey; 124 | ssKey >> vchPubKey; 125 | if (!vchPubKey.IsValid()) 126 | { 127 | strErr = "Error reading wallet database: CPubKey corrupt"; 128 | return false; 129 | } 130 | CKey key; 131 | CPrivKey pkey; 132 | uint256 hash; 133 | if (strType == "key") 134 | { 135 | wss.nKeys++; 136 | ssValue >> pkey; 137 | } else { 138 | CWalletKey wkey; 139 | ssValue >> wkey; 140 | pkey = wkey.vchPrivKey; 141 | } 142 | 143 | 从流中读取对应的哈希值。 144 | 145 | ssValue >> hash; 146 | 147 | 如果哈希不空,则处理如下: 148 | 149 | if (!hash.IsNull()) 150 | { 151 | std::vector vchKey; 152 | vchKey.reserve(vchPubKey.size() + pkey.size()); 153 | vchKey.insert(vchKey.end(), vchPubKey.begin(), vchPubKey.end()); 154 | vchKey.insert(vchKey.end(), pkey.begin(), pkey.end()); 155 | 156 | if (Hash(vchKey.begin(), vchKey.end()) != hash) 157 | { 158 | strErr = "Error reading wallet database: CPubKey/CPrivKey corrupt"; 159 | return false; 160 | } 161 | 162 | fSkipCheck = true; 163 | } 164 | 165 | 调用 `CKey` 对象的 `Load` 方法加载密钥。 166 | 167 | if (!key.Load(pkey, vchPubKey, fSkipCheck)) 168 | { 169 | strErr = "Error reading wallet database: CPrivKey corrupt"; 170 | return false; 171 | } 172 | 173 | 调用钱包对象的 `LoadKey` 方法,加载密钥。 174 | 175 | if (!pwallet->LoadKey(key, vchPubKey)) 176 | { 177 | strErr = "Error reading wallet database: LoadKey failed"; 178 | return false; 179 | } 180 | 181 | - 否则,如果当前的 Key 是 `mkey`,则从流中读取对应的主密钥,并保存在钱包 `mapMasterKeys` 对应的位置。 182 | 183 | else if (strType == "mkey") 184 | { 185 | unsigned int nID; 186 | ssKey >> nID; 187 | CMasterKey kMasterKey; 188 | ssValue >> kMasterKey; 189 | if(pwallet->mapMasterKeys.count(nID) != 0) 190 | { 191 | strErr = strprintf("Error reading wallet database: duplicate CMasterKey id %u", nID); 192 | return false; 193 | } 194 | pwallet->mapMasterKeys[nID] = kMasterKey; 195 | if (pwallet->nMasterKeyMaxID < nID) 196 | pwallet->nMasterKeyMaxID = nID; 197 | } 198 | 199 | - 否则,如果当前的 Key 是 `ckey`,则从流中读取对应的公钥、私钥,并调用钱包对象的 `LoadCryptedKey` 方法,加载加密的密钥。 200 | 201 | else if (strType == "ckey") 202 | { 203 | CPubKey vchPubKey; 204 | ssKey >> vchPubKey; 205 | if (!vchPubKey.IsValid()) 206 | { 207 | strErr = "Error reading wallet database: CPubKey corrupt"; 208 | return false; 209 | } 210 | std::vector vchPrivKey; 211 | ssValue >> vchPrivKey; 212 | wss.nCKeys++; 213 | 214 | if (!pwallet->LoadCryptedKey(vchPubKey, vchPrivKey)) 215 | { 216 | strErr = "Error reading wallet database: LoadCryptedKey failed"; 217 | return false; 218 | } 219 | wss.fIsEncrypted = true; 220 | } 221 | 222 | - 否则,如果当前的 Key 是 `keymeta`,那么从流中读取对应的公钥及其元数据,调用钱包对象的 `LoadKeyMetadata` 方法,加载密钥元数据。 223 | 224 | else if (strType == "keymeta") 225 | { 226 | CPubKey vchPubKey; 227 | ssKey >> vchPubKey; 228 | CKeyMetadata keyMeta; 229 | ssValue >> keyMeta; 230 | wss.nKeyMeta++; 231 | pwallet->LoadKeyMetadata(vchPubKey.GetID(), keyMeta); 232 | } 233 | 234 | - 否则,如果当前的 Key 是 `watchmeta`,那么从流中读取脚本和对应的元数据,并调用钱包对象的 `LoadScriptMetadata` 方法,加载脚本的元数据。 235 | 236 | else if (strType == "watchmeta") 237 | { 238 | CScript script; 239 | ssKey >> script; 240 | CKeyMetadata keyMeta; 241 | ssValue >> keyMeta; 242 | wss.nKeyMeta++; 243 | pwallet->LoadScriptMetadata(CScriptID(script), keyMeta); 244 | } 245 | 246 | - 否则,如果当前的 Key 是 `defaultkey`,那么从流中读取对应的公钥。 247 | 248 | else if (strType == "defaultkey") 249 | { 250 | CPubKey vchPubKey; 251 | ssValue >> vchPubKey; 252 | if (!vchPubKey.IsValid()) { 253 | strErr = "Error reading wallet database: Default Key corrupt"; 254 | return false; 255 | } 256 | } 257 | 258 | - 否则,如果当前的 Key 是 `pool`,那么从流中读取对应的密钥池,并调用钱包对象的 `LoadKeyPool` 方法,加载密钥池。 259 | 260 | else if (strType == "pool") 261 | { 262 | int64_t nIndex; 263 | ssKey >> nIndex; 264 | CKeyPool keypool; 265 | ssValue >> keypool; 266 | pwallet->LoadKeyPool(nIndex, keypool); 267 | } 268 | 269 | - 否则,如果当前的 Key 是 `version`,那么从流中读取对应的版本到钱包扫描状态对象的 `nFileVersion` 属性中。如果这个值等于 10300,那么设置钱包对象的这个属性为 300。 270 | 271 | else if (strType == "version") 272 | { 273 | ssValue >> wss.nFileVersion; 274 | if (wss.nFileVersion == 10300) 275 | wss.nFileVersion = 300; 276 | } 277 | 278 | - 否则,如果当前的 Key 是 `cscript`,那么从流中读取对应的脚本,并调用钱包对象的 `LoadCScript` 方法加载脚本。 279 | 280 | else if (strType == "cscript") 281 | { 282 | uint160 hash; 283 | ssKey >> hash; 284 | CScript script; 285 | ssValue >> script; 286 | if (!pwallet->LoadCScript(script)) 287 | { 288 | strErr = "Error reading wallet database: LoadCScript failed"; 289 | return false; 290 | } 291 | } 292 | 293 | - 否则,如果当前的 Key 是 `orderposnext`,那么从流中读取对应的值到钱包对象的 `nOrderPosNext` 属性中。 294 | 295 | else if (strType == "orderposnext") 296 | { 297 | ssValue >> pwallet->nOrderPosNext; 298 | } 299 | 300 | - 否则,如果当前的 Key 是 `destdata`,那么从流中读取对应的数据,并调用钱包对象的 `LoadDestData` 方法,处理这些数据。 301 | 302 | else if (strType == "destdata") 303 | { 304 | std::string strAddress, strKey, strValue; 305 | ssKey >> strAddress; 306 | ssKey >> strKey; 307 | ssValue >> strValue; 308 | pwallet->LoadDestData(DecodeDestination(strAddress), strKey, strValue); 309 | } 310 | 311 | - 否则,如果当前的 Key 是 `hdchain`,那么从流中读取 HD 链,并调用钱包对象的 `SetHDChain`方法,设置钱包的 HD 链中。 312 | 313 | else if (strType == "hdchain") 314 | { 315 | CHDChain chain; 316 | ssValue >> chain; 317 | pwallet->SetHDChain(chain, true); 318 | } 319 | 320 | - 否则,如果当前的 Key 是 ``,那么调用钱包的 `` 方法,设置其标志。 321 | 322 | else if (strType == "flags") { 323 | uint64_t flags; 324 | ssValue >> flags; 325 | if (!pwallet->SetWalletFlags(flags, true)) { 326 | strErr = "Error reading wallet database: Unknown non-tolerable wallet flags found"; 327 | return false; 328 | } 329 | } 330 | 331 | - 否则,如果当前的 Key 不是 `bestblock`、`bestblock_nomerkle`、`minversion`、`acentry` ,则钱包扫描状态对象的 `m_unknown_records` 未知道记录属性加1。 332 | 333 | 5. 返回真。 334 | 335 | 336 | # 后记 337 | 338 | 由于本人水平所限,文中错误在所难免,欢迎您踊跃指出错误,在下感激不尽。我的微信联系方式:joepeak。 339 | 340 | 原创不易,尤其寒冬,欢迎赞助我一杯咖啡。 341 | 342 |
343 |
344 | drawing 345 |

比特币

346 |
347 |
348 | drawing 349 |

微信

350 |
351 |
352 | drawing 353 |

支付宝

354 |
355 |
356 | -------------------------------------------------------------------------------- /wallet/TopUpKeyPool.md: -------------------------------------------------------------------------------- 1 | # 写在前面 2 | 3 | 创建钱包和交易是比特币最重要的两方面,涉及到很多很多的内容,远非一篇文章能概括的完。上一篇从整体上讲解了钱包的创建流程,虽然已经很详细了,但还是漏掉了几个重要的方法,从本篇开始我们讲解这几个特别重要的方法。对钱包感兴趣的朋友一定不要错误。 4 | 5 | 6 | # TopUpKeyPool 填充密钥池 7 | 8 | 本方法在第一次创建时会执行,在升级钱包到 HD 时,也会执行。它被用来填充密钥池 keypool。下面我们来看下方法的执行。 9 | 10 | 1. 如果标志禁止私钥,则返回假。 11 | 12 | if (IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { 13 | return false; 14 | } 15 | 16 | 2. 如果钱包被锁定,则返回假。 17 | 18 | if (IsLocked()) 19 | return false; 20 | 21 | 3. 如果参数 `kpSize` 大于0,则设置变量 `nTargetSize` 为 `kpSize`;否则,设置为用户指定的值,或默认的 1000。 22 | 23 | unsigned int nTargetSize; 24 | if (kpSize > 0) 25 | nTargetSize = kpSize; 26 | else 27 | nTargetSize = std::max(gArgs.GetArg("-keypool", DEFAULT_KEYPOOL_SIZE), (int64_t) 0); 28 | 29 | 4. 计算内部、外部可用的密钥数量。 30 | 31 | int64_t missingExternal = std::max(std::max((int64_t) nTargetSize, (int64_t) 1) - (int64_t)setExternalKeyPool.size(), (int64_t) 0); 32 | int64_t missingInternal = std::max(std::max((int64_t) nTargetSize, (int64_t) 1) - (int64_t)setInternalKeyPool.size(), (int64_t) 0); 33 | 34 | 在用户不指定的情况下,并且是第一次,因为 `setExternalKeyPool`、`setInternalKeyPool` 这两个集合都为空,所以 `missingExternal`、`missingInternal` 两个变量的值都为 1000。 35 | 36 | 5. 如果不支持 HD 钱包,或者不支持 HD 分割,那么设置变量 `missingInternal` 为0。 37 | 38 | if (!IsHDEnabled() || !CanSupportFeature(FEATURE_HD_SPLIT)) 39 | { 40 | missingInternal = 0; 41 | } 42 | 43 | 6. 生成访问数据库的对象。 44 | 45 | bool internal = false; 46 | WalletBatch batch(*database); 47 | 48 | 7. 进行 `for (int64_t i = missingInternal + missingExternal; i--;)` 循环。 49 | 50 | - 如果 `i` 小于 `missingInternal` ,则设置变量 `internal` 为真。 51 | 52 | if (i < missingInternal) { 53 | internal = true; 54 | } 55 | 56 | - 设置当前索引。 57 | 58 | int64_t index = ++m_max_keypool_index; 59 | 60 | - 生成公钥对象。 61 | 62 | CPubKey pubkey(GenerateNewKey(batch, internal)); 63 | 64 | `GenerateNewKey` 方法用来生成公钥。我们来看下这个方法的执行流程。 65 | 66 | - 生成表示创建的公钥是否为压缩的变量 `fCompressed`。在 0.6 版本之后的公钥默认都是压缩的。 67 | 68 | bool fCompressed = CanSupportFeature(FEATURE_COMPRPUBKEY); 69 | 70 | - 创建私钥对象和密钥元数据对象。这时候私钥和密钥元数据对象都没有经过设置,还不能成为真正的私钥和密钥元数据,只有经过下面两个方法中的某一个处理之后,才变成真正可用的对象。 71 | 72 | CKey secret; 73 | int64_t nCreationTime = GetTime(); 74 | CKeyMetadata metadata(nCreationTime); 75 | 76 | - 如果钱包支持 HD,那么调用 `DeriveNewChildKey` 方法来衍生子私钥,否则,调用 `MakeNewKey` 方法来生成私钥。 77 | 78 | if (IsHDEnabled()) { 79 | DeriveNewChildKey(batch, metadata, secret, (CanSupportFeature(FEATURE_HD_SPLIT) ? internal : false)); 80 | } else { 81 | secret.MakeNewKey(fCompressed); 82 | } 83 | 84 | `IsHDEnabled` 是通过私钥对象的 `hdChain.seed_id` 对象是否为空来判断的,因为在前面生成钱包私钥之后,就设置了这个方法,所以这个方法一定返回真。 85 | 86 | `MakeNewKey` 这个方法,我们在前面创建钱包的私钥时已经看到过,`DeriveNewChildKey` 这个方法我们在下面进行重点讲解,这里暂且略过。 87 | 88 | - 如果变量 `fCompressed` 为真,则调用 `SetMinVersion` 方法,设置最小版本为 `FEATURE_COMPRPUBKEY`,支持压缩公钥的版本。 89 | 90 | if (fCompressed) { 91 | SetMinVersion(FEATURE_COMPRPUBKEY); 92 | } 93 | 94 | - 调用私钥的 `GetPubKey` 方法,返回对应的公钥。 95 | 96 | 方法内部使用椭圆曲线算法求得公钥。具体不细讲,读者自行看代码。 97 | 98 | CPubKey CKey::GetPubKey() const { 99 | assert(fValid); 100 | secp256k1_pubkey pubkey; 101 | size_t clen = CPubKey::PUBLIC_KEY_SIZE; 102 | CPubKey result; 103 | int ret = secp256k1_ec_pubkey_create(secp256k1_context_sign, &pubkey, begin()); 104 | assert(ret); 105 | secp256k1_ec_pubkey_serialize(secp256k1_context_sign, (unsigned char*)result.begin(), &clen, &pubkey, fCompressed ? SECP256K1_EC_COMPRESSED : SECP256K1_EC_UNCOMPRESSED); 106 | assert(result.size() == clen); 107 | assert(result.IsValid()); 108 | return result; 109 | } 110 | 111 | - 把密钥元数据加入 `mapKeyMetadata` 集合中。 112 | 113 | mapKeyMetadata[pubkey.GetID()] = metadata; 114 | 115 | - 调用 `UpdateTimeFirstKey` 方法,更新 `nTimeFirstKey` 属性。 116 | 117 | UpdateTimeFirstKey(nCreationTime); 118 | 119 | - 调用 `AddKeyPubKeyWithDB` 方法,把私钥和公钥保存到数据库中。这个方法在前面已经讲过,这里再讲了。 120 | 121 | if (!AddKeyPubKeyWithDB(batch, secret, pubkey)) { 122 | throw std::runtime_error(std::string(__func__) + ": AddKey failed"); 123 | } 124 | 125 | - 返回公钥。 126 | 127 | - 用公钥生成一个密钥池实体 `CKeyPool` 对象,并调用访问钱包数据库对象的 `WritePool` 方法,以 `pool` 为键把密钥池实体对象写入数据库。 128 | 129 | if (!batch.WritePool(index, CKeyPool(pubkey, internal))) { 130 | throw std::runtime_error(std::string(__func__) + ": writing generated key failed"); 131 | } 132 | 133 | - 如果变量 `internal` 为真,则把索引保存到 `setInternalKeyPool` 集合中,否则,保存到 `setExternalKeyPool` 集合中。 134 | 135 | if (internal) { 136 | setInternalKeyPool.insert(index); 137 | } else { 138 | setExternalKeyPool.insert(index); 139 | } 140 | 141 | - 把索引保存到 `m_pool_key_to_index` 集合中。 142 | 143 | m_pool_key_to_index[pubkey.GetID()] = index; 144 | 145 | 8. 返回真。 146 | 147 | 148 | ## DeriveNewChildKey 衍生子密钥 149 | 150 | 这个方法用来衍生子密钥。方法的逻辑如下: 151 | 152 | 1. 生成相关的变量。 153 | 154 | CKey seed; //seed (256bit) 155 | CExtKey masterKey; //hd master key 156 | CExtKey accountKey; //key at m/0' 157 | CExtKey chainChildKey; //key at m/0'/0' (external) or m/0'/1' (internal) 158 | CExtKey childKey; //key at m/0'/0'/' 159 | 160 | 2. 调用 `GetKey` 方法,根据HD 链对象中保存的公钥对象来得到对应的私钥对象。这个私钥是根私钥,也被称为种子私钥。如果获得不到,则抛出异常。 161 | 162 | if (!GetKey(hdChain.seed_id, seed)) 163 | throw std::runtime_error(std::string(__func__) + ": seed not found"); 164 | 165 | `GetKey` 方法执行流程如下: 166 | 167 | - 如果钱包没有加密,则调用 `CBasicKeyStore::GetKey` 方法,返回公钥对象的私钥。 168 | 169 | if (!IsCrypted()) { 170 | return CBasicKeyStore::GetKey(address, keyOut); 171 | } 172 | 173 | `CBasicKeyStore::GetKey` 方法直接从 `mapKeys` 集合中取出对应的私钥。 174 | 175 | `mapKeys` 集合是我们前面分析 `DeriveNewSeed` 这个方法的第 6 步 `AddKeyPubKey` 这个方法中把私钥/公钥及元数据保存到数据库过程中设置的。 176 | 177 | - 否则,直接从加密集合 `mapCryptedKeys` 中取得对应的私钥。 178 | 179 | CryptedKeyMap::const_iterator mi = mapCryptedKeys.find(address); 180 | if (mi != mapCryptedKeys.end()) 181 | { 182 | const CPubKey &vchPubKey = (*mi).second.first; 183 | const std::vector &vchCryptedSecret = (*mi).second.second; 184 | return DecryptKey(vMasterKey, vchCryptedSecret, vchPubKey, keyOut); 185 | } 186 | 187 | - 如果以上两种情况都不能返回,那么只能返回假了。 188 | 189 | 3. 调用主私钥(扩展私钥)的 `SetSeed` 方法,设置种子。此时的主私钥只是一个对象,而不能当作真正的私钥来使用,因为内部数据不存在。只有在本方法调用之后,主私钥才能真正用来衍生密钥。 190 | 191 | 我们现在来看下 `SetSeed` 方法的逻辑: 192 | 193 | - 首先,生成需要的变量。 194 | 195 | static const unsigned char hashkey[] = {'B','i','t','c','o','i','n',' ','s','e','e','d'}; 196 | std::vector> vout(64); 197 | 198 | - 其次,调用 `CHMAC_SHA512` 方法,使用 `HMAC_SHA512` 算法,根据种子私钥生成长度为 512 位的字符串。 199 | 200 | CHMAC_SHA512(hashkey, sizeof(hashkey)).Write(seed, nSeedLen).Finalize(vout.data()); 201 | 202 | - 然后,调用私钥的 `Set` 方法,用 512 位的字符串的左边 256 位来初始化私钥的 `keydata` 属性,并且设置私钥的有效标志 `fValid` 属性为真,压缩标志 `fCompressed` 为参数指定的值,默认为真。 203 | 204 | key.Set(vout.data(), vout.data() + 32, true); 205 | 206 | - 调用 `memcpy` 方法,把 512 位的字符串的右边 256 位保存为链码。 207 | 208 | memcpy(chaincode.begin(), vout.data() + 32, 32); 209 | 210 | - 设置主私钥的 `nDepth`、`nChild` 为 0。 211 | 212 | nDepth = 0; 213 | nChild = 0; 214 | 215 | - 把 `vchFingerprint` 重置为 0。 216 | 217 | memset(vchFingerprint, 0, sizeof(vchFingerprint)); 218 | 219 | 至此,主私钥终于可用了。 220 | 221 | 4. 调用主私钥(扩展私钥)的 `Derive` 方法,开始衍生子密钥 `accountKey`。 222 | 223 | masterKey.Derive(accountKey, BIP32_HARDENED_KEY_LIMIT); 224 | 225 | 注意,在 bip32 之后,采用硬化衍生子密钥。 226 | 227 | 我们来看下 `Derive` 这个方法的执行流程: 228 | 229 | - 设置扩展私钥 `out` 参数的 `nDepth` 为 `nDepth` 加 1。主私钥的 `nDepth` 为 0,参见上面所讲。 230 | 231 | - 获取主公钥的 CKeyID。 232 | 233 | CKeyID id = key.GetPubKey().GetID(); 234 | 235 | - 设置置扩展私钥 `out` 参数的 `nChild` 为参数 `_nChild` 的值,这里为 `0x80000000`,10进制为 2147483648。 236 | 237 | - 调用根私钥的 `Derive` 方法,开始衍生私钥。 238 | 239 | return key.Derive(out.key, out.chaincode, _nChild, chaincode); 240 | 241 | 这个方法内部执行流程如下: 242 | 243 | - 生成一个向量。 244 | 245 | std::vector> vout(64); 246 | 247 | - 把变量 `nChild` 向右移动 31位,如果所得等于 0,那么调用 `GetPubKey` 方法,获得私钥对应的公钥,然后调用 `BIP32Hash` 方法,填充变量 `vout`。在 `BIP32Hash` 方法中使用 `CHMAC_SHA512` 方法,根据链码参数和子密钥数量来填充 `vout` 变量。如果右移所得不等于0,同样调用 `BIP32Hash` 方法,填充变量 `vout`。 248 | 249 | if ((nChild >> 31) == 0) { 250 | CPubKey pubkey = GetPubKey(); 251 | assert(pubkey.size() == CPubKey::COMPRESSED_PUBLIC_KEY_SIZE); 252 | BIP32Hash(cc, nChild, *pubkey.begin(), pubkey.begin()+1, vout.data()); 253 | } else { 254 | assert(size() == 32); 255 | BIP32Hash(cc, nChild, 0, begin(), vout.data()); 256 | } 257 | 258 | - 把变量 `vout` 的内容拷贝到子链码中。 259 | 260 | memcpy(ccChild.begin(), vout.data()+32, 32); 261 | 262 | - 把私钥的内容拷贝到子私钥中。 263 | 264 | memcpy((unsigned char*)keyChild.begin(), begin(), 32); 265 | 266 | - 使用椭圆曲线算法真正初始化子私钥。 267 | 268 | bool ret = secp256k1_ec_privkey_tweak_add(secp256k1_context_sign, (unsigned char*)keyChild.begin(), vout.data()); 269 | 270 | - 设置子私钥为压缩的,是否是有效的,并返回。 271 | 272 | keyChild.fCompressed = true; 273 | keyChild.fValid = ret; 274 | return ret; 275 | 276 | 5. `accountKey` 私钥(扩展私钥)的 `Derive` 方法,开始衍生子密钥 `chainChildKey`。方法前面刚讲过,此处略过。 277 | 278 | accountKey.Derive(chainChildKey, BIP32_HARDENED_KEY_LIMIT+(internal ? 1 : 0)); 279 | 280 | 6. 接下来衍生子私钥 `childKey`,与上面大致相同,可自行阅读。 281 | 282 | do { 283 | if (internal) { 284 | chainChildKey.Derive(childKey, hdChain.nInternalChainCounter | BIP32_HARDENED_KEY_LIMIT); 285 | metadata.hdKeypath = "m/0'/1'/" + std::to_string(hdChain.nInternalChainCounter) + "'"; 286 | hdChain.nInternalChainCounter++; 287 | } 288 | else { 289 | chainChildKey.Derive(childKey, hdChain.nExternalChainCounter | BIP32_HARDENED_KEY_LIMIT); 290 | metadata.hdKeypath = "m/0'/0'/" + std::to_string(hdChain.nExternalChainCounter) + "'"; 291 | hdChain.nExternalChainCounter++; 292 | } 293 | } while (HaveKey(childKey.key.GetPubKey().GetID())); 294 | 295 | 7. 设置变量 `secret` 的值为子私钥的 `childKey`。 296 | 297 | secret = childKey.key; 298 | 299 | 8. 设置变量 `metadata` 的 HD 种子为当前 HD 链的的种子。 300 | 301 | metadata.hd_seed_id = hdChain.seed_id; 302 | 303 | 9. 更新 HD 链到数据库中。 304 | 305 | if (!batch.WriteHDChain(hdChain)) 306 | throw std::runtime_error(std::string(__func__) + ": Writing HD chain model failed"); 307 | 308 | 309 | # 后记 310 | 311 | 由于本人水平所限,文中错误在所难免,欢迎您踊跃指出错误,在下感激不尽。我的微信联系方式:joepeak。 312 | 313 | 原创不易,尤其寒冬,欢迎赞助我一杯咖啡。 314 | 315 |
316 |
317 | drawing 318 |

比特币

319 |
320 |
321 | drawing 322 |

微信

323 |
324 |
325 | drawing 326 |

支付宝

327 |
328 |
329 | -------------------------------------------------------------------------------- /wallet/getnewaddress.md: -------------------------------------------------------------------------------- 1 | # 生成地址 2 | 3 | 如果有人想发送比特币给你,或者你从别人那里买几个比特币,就要把地址给对方,对方才能把币打到你指定的地址上。那么,如何才能拥有一个地址呢,下面我们就来讲讲这个问题。 4 | 5 | 比特币核心提供了很多 RPC 来供客户端调用,其中一个就是我们这里要讲的 `getnewaddress` 生成一个新的地址,通过这个 RPC ,我们就可以生成一个新的地址,有了这个地址,别人就可以给我们转账了。 6 | 7 | `getnewaddress` RPC 可以接收两个参数,第一个地址的标签,第二个是地址的类型。如果没有提供标签,那么默认的标签就是空,地址的类型当前支持:legacy、p2sh-segwit、bech32,默认类型由 `-addresstype` 参数指定,当前为 p2sh-segwit。 8 | 9 | 10 | 如果我们想看下这个 RPC 的帮助文档,可以执行如下的命令: 11 | 12 | ./src/bitcoin-cli -regtest help getnewaddress 13 | 14 | 就会显示帮助信息 15 | 16 | 这个 RPC 对应的方法实现位于 `src/wallet/rpcwallet.cpp` 文件,方法名称就是 RPC 名称,下面我们来看这个方法。 17 | 18 | ## 生成地址流程 19 | 20 | 1. 根据请求参数获得对应的钱包。 21 | 22 | std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); 23 | CWallet* const pwallet = wallet.get(); 24 | 25 | `GetWalletForJSONRPCRequest` 方法内部实现如下: 26 | 27 | - 调用 `GetWalletNameFromJSONRPCRequest` 方法,从请求对象中取得钱包的名字,如果用户指定了钱包名字,那么把钱包名字保存在参数 `wallet_name` 上,并返回真,否则返回假。 28 | 29 | - 如果可以获得用户指定的钱包名称,则调用 `GetWallet` 方法,从钱包集合 `vpwallets` 中取得指定的钱包,然后返回钱包。 30 | 31 | - 如果用户没有指定钱包或指定的钱包不存在,那么调用 `GetWallets` 方法,返回钱包集合 `vpwallets`。如果钱包集合中只有一个钱包,或者在用户指定了帮助的情况下,至少有一个以上的钱包,那么返回第一个钱包,即默认的钱包。默认钱包在系统启动时候创建的。 32 | 33 | 2. 接下来,要确保钱包可用。如果钱包不可用,则直接 `NullUniValue` 对象。 34 | 35 | if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) { 36 | return NullUniValue; 37 | } 38 | 39 | 3. 如果指定了 `help` 参数或请求参数数量多于2个,则显示钱包的帮助信息。 40 | 41 | 4. 检查钱包是否设置了禁止私钥,即钱包是只读的 `watch-only/pubkeys`。如果是,则抛出异常。 42 | 43 | if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { 44 | throw JSONRPCError(RPC_WALLET_ERROR, "Error: Private keys are disabled for this wallet"); 45 | } 46 | 47 | 5. 如果指定了标签,则调用 `LabelFromValue` 方法,检查标签,确保其不是 `*`。如果是,则抛出异常。 48 | 49 | std::string label; 50 | if (!request.params[0].isNull()) 51 | label = LabelFromValue(request.params[0]); 52 | 53 | 6. 如果指定了地址类型,则调用 `ParseOutputType` 方法,检查地址类型,确保其是 `legacy`、`p2sh-segwit`、`bech32` 之一,如果不指定则默认是 `p2sh-segwit`,并把地址类型保存在 `output_type` 变量中。 54 | 55 | OutputType output_type = pwallet->m_default_address_type; 56 | if (!request.params[1].isNull()) { 57 | if (!ParseOutputType(request.params[1].get_str(), output_type)) { 58 | throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Unknown address type '%s'", request.params[1].get_str())); 59 | } 60 | } 61 | 62 | 7. 如果钱包没有被锁定,则调用 `TopUpKeyPool` 方法填充密钥池。 63 | 64 | if (!pwallet->IsLocked()) { 65 | pwallet->TopUpKeyPool(); 66 | } 67 | 68 | `TopUpKeyPool` 填充密钥这个方法,我们前面已经讲过,这里只简单解释下,不做详细分析。因为在衍生子钥的过程中,`setExternalKeyPool`、`setInternalKeyPool` 已经完全填充完了,所以导致 `missingExternal`、`missingInternal` 两个变量都为 0,从而不会重新再次衍生子密钥,所以实际上本方法在这里基本没有执行,而直接返回真。 69 | 70 | 8. 调用钱包的 `GetKeyFromPool` 方法,从密钥池中生成一个公钥。如果不能生成,则抛出异常。 71 | 72 | CPubKey newKey; 73 | if (!pwallet->GetKeyFromPool(newKey)) { 74 | throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, "Error: Keypool ran out, please call keypoolrefill first"); 75 | } 76 | 77 | `GetKeyFromPool` 方法,我们在下面详细讲解,此处略过。 78 | 79 | 9. 调用钱包对象的 `LearnRelatedScripts` 方法,对公钥的脚本进行处理。 80 | 81 | 方法内部执行如下: 82 | 83 | 如果公钥是压缩的,并且地址类型是 `p2sh-segwit`,或者 `bech32`,那么: 84 | 85 | - 调用 `WitnessV0KeyHash` 方法,生成 `WitnessV0KeyHash` 对象。 86 | 87 | CTxDestination witdest = WitnessV0KeyHash(key.GetID()); 88 | 89 | - 调用 `GetScriptForDestination` 方法,获取对应的脚本。 90 | 91 | CScript witprog = GetScriptForDestination(witdest); 92 | 93 | `GetScriptForDestination` 方法内部调用 `boost::apply_visitor(CScriptVisitor(&script), dest)`,以访问者模式来根据不同的 id,获取其对应的脚本对象。 94 | 95 | `CScriptVisitor` 对象继承自 `boost::static_visitor` 对象,实现了访问者模式,并通过重载 `()` 操作符来定义不同类型的 id。 96 | 97 | - 如果目标参数类型是 `CNoDestination`,则调用脚本对象的 `script` 方法,清除脚本内容。 98 | 99 | - 如果目标参数类型是 `CKeyID`,则:首先调用脚本对象的 `script` 方法,清除脚本内容;然后,初始化脚本 `*script << OP_DUP << OP_HASH160 << ToByteVector(keyID) << OP_EQUALVERIFY << OP_CHECKSIG`。 100 | 101 | - 如果目标参数类型是 `CScriptID`,则:首先调用脚本对象的 `script` 方法,清除脚本内容;然后,初始化脚本 `*script << OP_HASH160 << ToByteVector(scriptID) << OP_EQUAL`。 102 | 103 | - 如果目标参数类型是 `WitnessV0KeyHash`,则:首先调用脚本对象的 `script` 方法,清除脚本内容;然后,初始化脚本 ` *script << OP_0 << ToByteVector(id)`。 104 | 105 | - 如果目标参数类型是 `WitnessV0ScriptHash`,则:首先调用脚本对象的 `script` 方法,清除脚本内容;然后,初始化脚本 `*script << OP_0 << ToByteVector(id)`。 106 | 107 | - 如果目标参数类型是 `WitnessUnknown`,则:首先调用脚本对象的 `script` 方法,清除脚本内容;然后,初始化脚本 `*script << CScript::EncodeOP_N(id.version) << std::vector(id.program, id.program + id.length)`。 108 | 109 | - 调用 `AddCScript` 方法,保存脚本对象。 110 | 111 | `AddCScript` 方法,首先调用 `CCryptoKeyStore::AddCScript` 方法,把脚本保存到 key store 的 `mapScripts` 集合中;然后,调用数据库访问对象的 `WriteCScript` 方法,以 `cscript` 为键把脚本保存到数据库中。 112 | 113 | bool CWallet::AddCScript(const CScript& redeemScript) 114 | { 115 | if (!CCryptoKeyStore::AddCScript(redeemScript)) 116 | return false; 117 | return WalletBatch(*database).WriteCScript(Hash160(redeemScript), redeemScript); 118 | } 119 | 120 | 10. 调用 `GetDestinationForKey` 方法,获取目的地 `CTxDestination` 对象。 121 | 122 | `CTxDestination` 是一个具有特定目标的交易输出脚本模板。 123 | 124 | 定义如下:`typedef boost::variant CTxDestination`,可能是以下几种类型之一: 125 | 126 | - CNoDestination 127 | 128 | 没有目的地设置 129 | 130 | - CKeyID 131 | 132 | P2PKH 目的 133 | 134 | - CScriptID 135 | 136 | P2SH 目的 137 | 138 | - WitnessV0ScriptHash 139 | 140 | P2WSH 目的 141 | 142 | - WitnessV0KeyHash 143 | 144 | P2WPKH 目的 145 | 146 | - WitnessUnknown 147 | 148 | 未知目的 P2W??? 149 | 150 | `GetDestinationForKey` 方法,使用 `case` 表达式来根据不同的地址类型,生成不同的目的 `CTxDestination`。 151 | 152 | - 如果地址类型是 `legacy`,则直接返回公钥的 `KeyID`。内部把公钥的数据通过 SHA256 和 RIPEMD160 双重哈希之后,构造一个 `CKeyID` 对象。 153 | 154 | - 如果地址类型是 `p2sh-segwit`,或 `bech32`,则处理如下: 155 | 156 | - 如果公钥不是压缩的,处理方法 `legacy`。 157 | 158 | if (!key.IsCompressed()) return key.GetID(); 159 | 160 | - 否则,生成 `WitnessV0KeyHash` 对象,然后调用 `GetScriptForDestination` 方法,获取对应的脚本,最后根据不同的地址类型生成的目的。 161 | 162 | CTxDestination witdest = WitnessV0KeyHash(key.GetID()); 163 | CScript witprog = GetScriptForDestination(witdest); 164 | if (type == OutputType::P2SH_SEGWIT) { 165 | return CScriptID(witprog); 166 | } else { 167 | return witdest; 168 | } 169 | 170 | 对于默认的、不传地址类型的情况,就会返回 `CScriptID` 类型的 `CTxDestination`,这个返回值在下面两步中都会用到。 171 | 172 | 11. 调用钱包对象的 `SetAddressBook` 方法,来保存公钥地址。 173 | 174 | pwallet->SetAddressBook(dest, label, "receive"); 175 | 176 | `SetAddressBook` 方法执行如下: 177 | 178 | - 从 `mapAddressBook` 集合中,取得对应的目的数据。 179 | 180 | std::map::iterator mi = mapAddressBook.find(address); 181 | 182 | - 根据集合中是否有对应的数据设置变量是否为更新。 183 | 184 | fUpdated = mi != mapAddressBook.end(); 185 | 186 | - 把标签保存为地址对应的 `CAddressBookData` 的 `name` 属性。 187 | 188 | mapAddressBook[address].name = strName; 189 | 190 | - 如果参数 `strPurpose` 不空,则更新地址对应的 `CAddressBookData` 的 `purpose` 属性。 191 | 192 | if (!strPurpose.empty()) mapAddressBook[address].purpose = strPurpose; 193 | 194 | - 调用数据库访问对象的 `WritePurpose` 方法,保存参数 `strPurpose` 到数据库中。 195 | 196 | if (!strPurpose.empty() && !WalletBatch(*database).WritePurpose(EncodeDestination(address), strPurpose)) 197 | return false; 198 | 199 | - 调用数据库访问对象的 `WritePurpose` 方法,保存地址到数据库中。 200 | 201 | WalletBatch(*database).WriteName(EncodeDestination(address), strName); 202 | 203 | `strName` 为用户提供的标签。`EncodeDestination` 方法,我们在下一步讲解。 204 | 205 | 12. 调用 `EncodeDestination` 方法,解码目的地址,并返回其结果。 206 | 207 | `EncodeDestination` 方法同样采用了访问者模式 `return boost::apply_visitor(DestinationEncoder(Params()), dest)`。`DestinationEncoder` 类继承了 `boost::static_visitor`,实现了访问者模式,通过重载 `()` 操作符来定义不同类型的 id。 208 | 209 | 与前面相对应,这个方法会处理 `CKeyID`、`CScriptID`、`WitnessV0KeyHash`、`WitnessV0ScriptHash`、`WitnessUnknown` 这几种不同情况。对于我们的默认情况来说,目的地址类型为 `CScriptID`,下面我们就看下这种情况的处理,其他情况可自行阅读。 210 | 211 | - 调用当前网络参数的 `Base58Prefix` 方法,返回脚本前缀。 212 | 213 | std::vector data = m_params.Base58Prefix(CChainParams::SCRIPT_ADDRESS); 214 | 215 | 对于主网络前缀是 5,测试网络是 196,回归测试网络是 196。 216 | 217 | - 把当前 20 个字节的数据加在前缀后面形成 21 个字节的字符串。 218 | 219 | data.insert(data.end(), id.begin(), id.end()); 220 | 221 | - 调用 `EncodeBase58Check` 方法,编码成 Base58Check 格式,并返回其值。 222 | 223 | return EncodeBase58Check(data); 224 | 225 | 下面,我们来看下 `EncodeBase58Check` 这个方法的处理。它的内部执行流程如下:用 21 个字节的字符串生成一个向量,同时调用 `Hash` 方法,生成一个 32 字节长的哈希字符串;然后把其最前面的 4个字节作为校验各加在 21 个字节的向量尾部,从而生成一个长度为 25个字节的字符串;最后,调用 `EncodeBase58` 方法,进行 Base58 编码。 226 | 227 | std::vector vch(vchIn); 228 | uint256 hash = Hash(vch.begin(), vch.end()); 229 | vch.insert(vch.end(), (unsigned char*)&hash, (unsigned char*)&hash + 4); 230 | return EncodeBase58(vch); 231 | 232 | 在 `Hash` 这个方法中,使用了双重 SHA256 哈希算法。`EncodeBase58` 这个方法,读者可以自行阅读,这里不再展开。 233 | 234 | 235 | ## GetKeyFromPool 从密钥池中获取公钥 236 | 237 | 本方法从密钥池中生成一个公钥。第一个参数为公钥的引用,第二个参数 `internal`,默认为假。 238 | 239 | 内部逻辑如下: 240 | 241 | 1. 如果钱包禁止私钥,则返回假。 242 | 243 | if (IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { 244 | return false; 245 | } 246 | 247 | 2. 调用 `ReserveKeyFromKeyPool` 方法,从密钥池中取出一个密钥并获取其公钥。如果不成功,则生成数据库访问对象,然后调用 `GenerateNewKey` 方法,生成一个公钥。 248 | 249 | if (!ReserveKeyFromKeyPool(nIndex, keypool, internal)) { 250 | if (IsLocked()) return false; 251 | WalletBatch batch(*database); 252 | result = GenerateNewKey(batch, internal); 253 | return true; 254 | } 255 | 256 | `GenerateNewKey` 这个方法,在创建钱包过程中,我们已经重点分析,这里不浪费口舌,我们重点看下 `ReserveKeyFromKeyPool` 方法。这个方法的执行流程如下: 257 | 258 | - 生成一个公钥,并设置为密钥池的 `vchPubKey` 属性。 259 | 260 | nIndex = -1; 261 | keypool.vchPubKey = CPubKey(); 262 | 263 | - 如果钱包没有被锁,则填充密钥池。 264 | 265 | if (!IsLocked()) 266 | TopUpKeyPool(); 267 | 268 | `TopUpKeyPool` 这个方法,我们也讲过,这里直接略过。 269 | 270 | - 如果钱包启用了 HD,并且可以支持 HD 分割,并且参数 `fRequestedInternal` 为真,则设置变量 `fReturningInternal` 为真。在调用本方法时,这个参数没有指定,而默认为假,所以变量 `fRequestedInternal` 设置假。 271 | 272 | bool fReturningInternal = IsHDEnabled() && CanSupportFeature(FEATURE_HD_SPLIT) && fRequestedInternal; 273 | 274 | - 根据集合 `set_pre_split_keypool` 是否为空,设置变量 `use_split_keypool` 的值。因为这里 `use_split_keypool` 集合为空,所以变量 `use_split_keypool` 为真。 275 | 276 | bool use_split_keypool = set_pre_split_keypool.empty(); 277 | 278 | - 根据变量 `use_split_keypool`、`fReturningInternal` 确定从哪个集合中获取密钥池对象。根据上面分析,我们最终会从 `setExternalKeyPool` 集合中取数据。 279 | 280 | std::set& setKeyPool = use_split_keypool ? (fReturningInternal ? setInternalKeyPool : setExternalKeyPool) : set_pre_split_keypool; 281 | 282 | - 如果要数据的集合为为空,则返回假。 283 | 284 | if (setKeyPool.empty()) { 285 | return false; 286 | } 287 | 288 | - 生成数据库访问对象。 289 | 290 | WalletBatch batch(*database); 291 | 292 | - 从 `setKeyPool` 取得其第一个元素,并从集合中删除它。 293 | 294 | auto it = setKeyPool.begin(); 295 | nIndex = *it; 296 | setKeyPool.erase(it); 297 | 298 | - 从数据库取得索引对应的密钥池。如果失败,则抛出异常。 299 | 300 | if (!batch.ReadPool(nIndex, keypool)) { 301 | throw std::runtime_error(std::string(__func__) + ": read failed"); 302 | } 303 | 304 | - 从密钥池中取得公钥对应的 ID,并且检测其是否在 `mapKeys`、或 `mapCryptedKeys` 集合之一,如果不在,则抛出异常。我们在创建钱包过程时候讲过,生成的私钥根据是否加密会保存在这两个集合之一。 305 | 306 | if (!HaveKey(keypool.vchPubKey.GetID())) { 307 | throw std::runtime_error(std::string(__func__) + ": unknown key in key pool"); 308 | } 309 | 310 | - 如果变量 `use_split_keypool` 为真,并且密钥池的 `fInternal` 属性不等于变量 `fReturningInternal`,那么抛出异常。 311 | 312 | if (use_split_keypool && keypool.fInternal != fReturningInternal) { 313 | throw std::runtime_error(std::string(__func__) + ": keypool entry misclassified"); 314 | } 315 | 316 | - 如果密钥池中保存的公钥是无效的,那么抛出异常。 317 | 318 | if (!keypool.vchPubKey.IsValid()) { 319 | throw std::runtime_error(std::string(__func__) + ": keypool entry invalid"); 320 | } 321 | 322 | - 从 `m_pool_key_to_index` 集合中消除对应的索引。 323 | 324 | m_pool_key_to_index.erase(keypool.vchPubKey.GetID()); 325 | 326 | - 返回真。 327 | 328 | 3. 调用 `KeepKey` 329 | 330 | 4. 从密钥池中取出对应的公钥。 331 | 332 | 333 | # 后记 334 | 335 | 由于本人水平所限,文中错误在所难免,欢迎您踊跃指出错误,在下感激不尽。我的微信联系方式:joepeak。 336 | 337 | 原创不易,尤其寒冬,欢迎赞助我一杯咖啡。 338 | 339 |
340 |
341 | drawing 342 |

比特币

343 |
344 |
345 | drawing 346 |

微信

347 |
348 |
349 | drawing 350 |

支付宝

351 |
352 |
353 | 354 | -------------------------------------------------------------------------------- /wallet/method.md: -------------------------------------------------------------------------------- 1 | GenerateNewKey 2 | 3 | DeriveNewChildKey 4 | 5 | AddKeyPubKeyWithDB 6 | 7 | AddKeyPubKey 8 | 9 | AddCryptedKey 10 | 11 | LoadKeyMetadata 12 | 13 | LoadScriptMetadata 14 | 15 | LoadCryptedKey 16 | 17 | UpdateTimeFirstKey 18 | 19 | AddCScript 20 | 21 | LoadCScript 22 | 23 | AddWatchOnly 24 | 25 | AddWatchOnly 26 | 27 | RemoveWatchOnly 28 | 29 | LoadWatchOnly 30 | 31 | Unlock 32 | 33 | ChangeWalletPassphrase 34 | 35 | ChainStateFlushed 36 | 37 | SetMinVersion 38 | 39 | SetMaxVersion 40 | 41 | GetConflicts 42 | 43 | HasWalletSpend 44 | 45 | Flush 46 | 47 | SyncMetaData 48 | 49 | IsSpent 50 | 51 | AddToSpends 52 | 53 | AddToSpends 54 | 55 | EncryptWallet 56 | 57 | ReorderTransactions 58 | 59 | IncOrderPosNext 60 | 61 | MarkDirty 62 | 63 | MarkReplaced 64 | 65 | AddToWallet 66 | 67 | LoadToWallet 68 | 69 | AddToWalletIfInvolvingMe 70 | 71 | TransactionCanBeAbandoned 72 | 73 | MarkInputsDirty 74 | 75 | AbandonTransaction 76 | 77 | MarkConflicted 78 | 79 | SyncTransaction 80 | 81 | TransactionAddedToMempool 82 | 83 | TransactionRemovedFromMempool 84 | 85 | BlockConnected 86 | 87 | BlockDisconnected 88 | 89 | BlockUntilSyncedToCurrentChain 90 | 91 | IsMine 92 | 93 | GetDebit 94 | 95 | IsMine 96 | 97 | GetCredit 98 | 99 | IsChange 100 | 101 | GetChange 102 | 103 | IsMine 104 | 105 | IsFromMe 106 | 107 | GetDebit 108 | 109 | IsAllFromMe 110 | 111 | GetCredit 112 | 113 | GetChange 114 | 115 | GenerateNewSeed 116 | 117 | DeriveNewSeed 118 | 119 | SetHDSeed 120 | 121 | SetHDChain 122 | 123 | IsHDEnabled 124 | 125 | SetWalletFlag 126 | 127 | SetWalletFlags 128 | 129 | GetTxTime 130 | 131 | DummySignInput 132 | 133 | DummySignTx 134 | 135 | CalculateMaximumSignedTxSize 136 | 137 | CalculateMaximumSignedTxSize 138 | 139 | CalculateMaximumSignedInputSize 140 | 141 | GetAmounts 142 | 143 | RescanFromTime 144 | 145 | ScanForWalletTransactions 146 | 147 | ReacceptWalletTransactions 148 | 149 | RelayWalletTransaction 150 | 151 | GetConflicts 152 | 153 | GetDebit 154 | 155 | GetCredit 156 | 157 | GetImmatureCredit 158 | 159 | GetAvailableCredit 160 | 161 | GetImmatureWatchOnlyCredit 162 | 163 | GetChange 164 | 165 | InMempool 166 | 167 | IsTrusted 168 | 169 | IsEquivalentTo 170 | 171 | ResendWalletTransactionsBefore 172 | 173 | ResendWalletTransactions 174 | 175 | 176 | 177 | @defgroup 178 | 179 | GetBalance 180 | 181 | GetUnconfirmedBalance 182 | 183 | GetImmatureBalance 184 | 185 | GetUnconfirmedWatchOnlyBalance 186 | 187 | GetImmatureWatchOnlyBalance 188 | 189 | GetLegacyBalance 190 | 191 | GetAvailableBalance 192 | 193 | AvailableCoins 194 | 195 | ListCoins 196 | 197 | FindNonChangeParentOutput 198 | 199 | SelectCoinsMinConf 200 | 201 | SelectCoins 202 | 203 | SignTransaction 204 | 205 | FundTransaction 206 | 207 | TransactionChangeType 208 | 209 | CreateTransaction 210 | 211 | CommitTransaction 212 | 213 | LoadWallet 214 | 215 | ZapSelectTx 216 | 217 | ZapWalletTx 218 | 219 | SetAddressBook 220 | 221 | DelAddressBook 222 | 223 | GetLabelName 224 | 225 | NewKeyPool 226 | 227 | KeypoolCountExternalKeys 228 | 229 | LoadKeyPool 230 | 231 | TopUpKeyPool 232 | 233 | ReserveKeyFromKeyPool 234 | 235 | KeepKey 236 | 237 | ReturnKey 238 | 239 | GetKeyFromPool 240 | 241 | GetOldestKeyTimeInPool 242 | 243 | GetOldestKeyPoolTime 244 | 245 | GetAddressBalances 246 | 247 | GetAddressGroupings 248 | 249 | GetLabelAddresses 250 | 251 | GetReservedKey 252 | 253 | KeepKey 254 | 255 | ReturnKey 256 | 257 | MarkReserveKeysAsUsed 258 | 259 | GetScriptForMining 260 | 261 | LockCoin 262 | 263 | UnlockCoin 264 | 265 | UnlockAllCoins 266 | 267 | IsLockedCoin 268 | 269 | ListLockedCoins 270 | 271 | 272 | 273 | 274 | GetKeyBirthTimes 275 | 276 | ComputeTimeSmart 277 | 278 | AddDestData 279 | 280 | EraseDestData 281 | 282 | LoadDestData 283 | 284 | GetDestData 285 | 286 | GetDestValues 287 | 288 | MarkPreSplitKeys 289 | 290 | Verify 291 | 292 | CreateWalletFromFile 293 | 294 | postInitProcess 295 | 296 | BackupWallet 297 | 298 | CKeyPool 299 | 300 | CWalletKey 301 | 302 | SetMerkleBranch 303 | 304 | GetDepthInMainChain 305 | 306 | GetBlocksToMaturity 307 | 308 | IsImmatureCoinBase 309 | 310 | AcceptToMemoryPool 311 | 312 | LearnRelatedScripts 313 | 314 | LearnAllRelatedScripts 315 | 316 | GroupOutputs 317 | 318 | GetKeyOrigin 319 | 320 | 321 | 322 | -------------------------------------------------------------------------------- /wallet/walletsummary.md: -------------------------------------------------------------------------------- 1 | # 引子 2 | 3 | 创建钱包及其重要的几个都写完了,但总感觉还缺少一些什么,到底缺少什么呢?在重新读过几边之后,才发现通过前面几篇文章还是不能把一切连贯起来,所以又补写了本篇,作为创建钱包的补充说明。 4 | 5 | # 正文 6 | 7 | 首先说明,下文中说的 `hash160 字符串` 指的是私钥或公钥的内容通过双重哈希算法(行 `SHA256`,再 `RIPEMD160`)生成的长度为 20 个字节的字符串。 8 | 9 | 钱包是 keystore 的扩展,管理着交易和余额,提供创建交易的能力。它有几个特别重要的属性,现在解释如下: 10 | 11 | - hdChain,HD 数据模型,它包含了一个 hash160 的种子,一个内部链的数量和一个外部链的数量。 12 | 13 | - setInternalKeyPool,内部密钥池的集合。 14 | 15 | - setExternalKeyPool,外部密钥池的集合。 16 | 17 | - set_pre_split_keypool,一个预分割的密钥池集合。 18 | 19 | - m_max_keypool_index,最大密钥池的索引。 20 | 21 | - m_pool_key_to_index 22 | 23 | - mapKeyMetadata,公钥元数据的映射集合。键为一个 hash160 字符串,值为一个公钥元数据。 24 | 25 | - m_script_metadata 26 | 27 | - mapMasterKeys, 28 | 29 | - mapWallet 30 | 31 | - mapAddressBook 32 | 33 | 34 | 钱包说完了,我们来看 keystore。keystore 顾名思议它代表了密钥的存储,自然而然提供了一些管理密钥的方法,比如:添加一个密钥(私钥和公钥)到 store中、检查给定地址对应的密钥是否在 store中、添加及检查一般脚本与只读脚本的功能等。根据是否加密,keystore 分为基础的和加密的两种。钱包继承自加密的 keystore,而加密的 keystore 又继承了基础的 keystore。 35 | 36 | 我们先来看下 基础 keystore ,它有以下几个重要的属性: 37 | 38 | - mapKeys,一个私钥映射集合。键为一个 hash160 字符串,值为一个私钥。 39 | 40 | - mapWatchKeys,一个只读的公钥映射集合。键为一个 hash160 字符串,值为一个公钥。 41 | 42 | - mapScripts,一个脚本映射集合。键为一个 hash160 字符串,值为一个用在交易输入和输出的序列化的脚本。 43 | 44 | - setWatchOnly,一个用在交易输入和输出的序列化的脚本集合。 45 | 46 | 加密 keystore 在基础 keystore 上增加了几个与加密相关的属性,它们分别为: 47 | 48 | - fUseCrypto,一个标志钱包是否为加密的变量。 49 | 50 | - vMasterKey,一个在加密情况下使用的私钥集合。当加密时 `mapKeys` 集合就会为空,而 `vMasterKey` 集合不空;当不加密时,情况正好反过来,即 `mapKeys` 集合不空,而 `vMasterKey` 集合为空。 51 | 52 | - mapCryptedKeys,一个映射集合。键为一个 hash160 字符串,值为一个公钥和加密后私钥组成的 pair 。 53 | 54 | 55 | 说完钱包与 keystore,下面我们就来看下密钥池。其实密钥池这个名字不准确,因为它仅包含了一个公钥,初次之外,还包含了两个布尔变量:`fInternal`、`m_pre_split`,前者表示密钥池是内部还是外部的,后者功能暂时不清楚,英语备注为:For keys generated before keypool split upgrade。`m_pre_split` 属性默认为假。 56 | 57 | 58 | 59 | 当创建钱包时,会执行如下几个动作: 60 | 61 | 1. 首先,生成一个私钥,根据私钥通过椭圆曲线算法生成对应的公钥; 62 | 63 | 2. 其次,也会生成对应的密钥元数据,并且把公钥的内容用 hash160 (先 SHA256,再RIPEMD160)算法生成的 20个字节的字符串保存为密钥元数据的种子,然后把私钥元数据保存在 `mapKeyMetadata` 集合中; 64 | 65 | 3. 然后,私钥被保存在 `mapKeys`,或 `mapCryptedKeys` 与 `vMasterKey` 集合中;当公钥是压缩的(通常是)时,通过公钥生成脚本,脚本进而被保存在 `mapScripts` 集合中;同时,私钥、公钥及密钥元数据都被保存在数据库中。 66 | 67 | 4. 再然后,公钥被作为种子,生成 HD 链对象,保存在钱包中。 68 | 69 | 5. 最后,当前面一切完成后,使用前面 3步生成的公钥做为种子,开始衍生用户指定数量的子私钥/公钥对,如果用户没有指定则默认衍生 3000 个子私钥/公钥对。 70 | 71 | - 衍生的私钥、公钥及元数据的处理与第 2、3 步相同; 72 | 73 | - 同时,用公钥生成的密钥池也被保存在数据库中; 74 | 75 | - 根据生成的私钥属于内部或外部,对应的索引保存在 `setInternalKeyPool`、或 `setExternalKeyPool` 集合中;不区分地,索引被保存在 `m_pool_key_to_index` 映射集合中,其中键为公钥对应的 hash160 字符串。 76 | 77 | > 其中,2000 个子私钥的路径从 `m/0'/0'/0` 到 `m/0'/0'/1999`,1000 个子私钥的路径从 `m/0'/1'/0` 到 `m/0'/1'/999`。其中的 `m` 代表私钥,`m/0'` 代表主私钥的第 1 个强化子私钥,`m/0'/0'/0` 代表主私钥的第 1 个强化子私钥的外部链的第 1 个强化孙私钥,同理,`m/0'/0'/1999` 代表主私钥的第 1 个强化子私钥的外部链的第 1999 个强化孙私钥;`m/0'/1'/0` 代表主私钥的第 1 个强化子私钥的内部链的第 1 个强化孙私钥,同理,`m/0'/1'/999` 代表主私钥的第 1 个强化子私钥的内部链的第 999 个强化孙私钥。 78 | 79 | 80 | 在上面过程中,1-3 步是 `GenerateNewSeed` 方法的主要内容,第 4 步是 `SetHDSeed` 的主要内容,第 5 步是 `TopUpKeyPool` 的主要内容。 81 | 82 | 最后,用两张图来概述 HD 钱包的创建。 83 | 84 | ![创建钱包](http://blockchain.szdyfjh.com/generate-hd-wallet.jpg) 85 | 86 | ![创建钱包](http://blockchain.szdyfjh.com/generate-hd-wallet.png) 87 | 88 | 89 | # 后记 90 | 91 | 由于本人水平所限,文中错误在所难免,欢迎您踊跃指出错误,在下感激不尽。我的微信联系方式:joepeak。 92 | 93 | 原创不易,尤其寒冬,欢迎赞助我一杯咖啡。 94 | 95 |
96 |
97 | drawing 98 |

比特币

99 |
100 |
101 | drawing 102 |

微信

103 |
104 |
105 | drawing 106 |

支付宝

107 |
108 |
109 | --------------------------------------------------------------------------------