本系列博文采用的比特币源码版本是0.11.3。下载源码包后解压,源代码在src目录以及子目录下。下面的表格对src目录下的文件做个简单的介绍(file.* 的含义是包含头文件file.h和源文件file.cpp):
https://www.jianshu.com/u/336e6a2afb16
文件 | 简介 |
---|---|
net.* | 管理比特币网络上节点的连接以及节点之间数据的发送和接收 |
init.cpp | 对比特币节点进行初始化,整个初始化分为12个步骤进行。 |
main.* | main.h 声明了很多全局的变量(比如mapBlockIndex, chainActive, mempool)和全局的函数 main.cpp 包含了很多管理区块的函数,比如验证和存储区块。main.cpp中的很多函数会在节点初始化的时候,在init.cpp中调用。 |
chain.* | chain.h声明了用来表示区块头的类CBlockIndex chain.cpp 包含了一些手动管理区块链的函数,比如定位一个区块以及找到两条区块链的分叉点 |
coins.* | coins.h中定义了CCoin类 |
miner.* | 包含挖矿以及打包区块的代码 |
下面的表格是对src下的子目录的简介:
子目录 | 简介 |
---|---|
leveldb | 存放LevelDB的源码,编译时会用到 |
qt | 存放GUI的代码,图形界面采用QT实现,所以目录名为qt |
secp256k1 | 用来实现ECDSA加密算法的C语言库 |
zmq | 存放ZMQ的源码 |
consensus | 包含区块和交易的验证规则 |
crypto | 包含加密hash算法,比如sha-256、base58 |
script | 比特币的脚本引擎 |
primitives | 定义了基础的数据类型,比如区块和交易 |
wallet | 钱包相关的代码 |
比特币的启动入口在bitcoind.cpp中,在main()中回执行三个函数:
SetupEnvironment:设置locale
noui_connect:绑定信号量
AppInit:对程序进行初始化,共分为12个步骤
下面的表格是对初始化中12个步骤的简单介绍
步骤 | 简介 | 描述 |
---|---|---|
1 | 跨平台兼容设置 | 根据操作系统的不同,进行必要的设置以增加程序的兼容性 |
2 | 参数解析 | 经过解析后,参数和对应的值保存在变量mapArgs和mapMultiArgs中。
在该步骤中,对网络配置相关的参数分别作处理。 |
3 | 对钱包相关参数设置 | 对钱包相关参数设置 |
4 | 加锁/开启RPC | 1.给存放数据的目录加锁,确保只有一个比特币进程使用该数据目录
2.创建多个子线程,执行ThreadScriptCheck(脚本检查) 3.创建任务调度线程serviceLoop 4.开启RPC服务 |
5 | 验证钱包的完整性 | 如果开启钱包功能,则打开钱包文件,验证钱包的完整性 |
6 | 网络初始化 | 1.给节点注册信号量处理函数
2.设置网络相关的配置,比如监听端口,代理等。 |
7 | 加载区块链 | 1.设置各种cache的大小
2.加载区块索引到内存 3.如果没有创世区块,那么初始化区块索引 |
8 | 加载钱包 | 如果开启了钱包功能,则加载钱包 |
9 | 如果fPruneMode设置为true,则调用PruneAndFlush函数 | |
10 | 导入区块 | 开启新的线程,加载最长的区块链 |
11 | 启动节点 | 1.开启线程bitcoin-dnsseed,从硬编码的dns种子中加载相邻节点地址
2.开启线程bitcoin-net,管理和相邻节点之间连接的建立和关闭 3.开启线程bitcoin-addcon,用来初始化和用户指定节点之间的连接 4.开启线程bitcoin-opencon ,用来初始化从dns种子中获得的节点之间的连接 5.开启线程bitcoin-msghand,用来和相邻节点发送和接受消息。 6.每隔900毫秒向任务调度器中添加任务,该任务的内容是向peers.dat中写入新的比特币网络中的节点地址 |
12 | 完成 | 完成 |
你的位置:欧阳慕峰的Blog > Bitcoin > 比特币源码剖析(二)之数据存储
比特币源码剖析(二)之数据存储
Bitcoin justnode 10个月前 (03-10) 154浏览
在比特币中,有四种数据被持久化存储在硬盘上:
1.block/blk*.dat:这些文件存储的是真正的区块链数据,以二进制的形式存储在磁盘上。当需要跟其他节点同步区块数据以及在钱包中搜索缺失的交易时才会用到。
2.block/index/*.ldb: 数据存储在leveldb数据库中,里面包含所有区块的区块索引。每个区块索引只有88字节,截止到2018年3月,全部区块索引的大小为80M,远小于区块的总大小。
3.block/rev*.dat: 存储未交易数据,当区块链需要回滚时会用到。
4.chainstate/*.ldb:数据存储在leveldb数据库中,里面包含当前所有的未花费的交易输出。
blk*.dat
每一个blk*.dat文件的大小是128M,截止到2018年3月10号,最新的区块文件是blk1168.dat,总大小是150G。
每一个区块文件(比如blk00001.dat)都有一个与之相对应的未交易文件rev*.dat(比如rev00001.dat)。区块文件的信息存储在leveldb数据库中,分为两部分:
在比特币代码中想要读取区块文件的内容,需要如下两个变量:
1.DiskBlockPos:这是指向区块文件所在位置的结构体变量,包含两个元素:filename和offset
2.vInfoBlockFiles:这是一个元素类型为BlockFileInfo的vector变量。
区块索引(block index)
区块索引包含所有区块的mete信息。注意这里的“所有区块”不仅仅是指最长区块链上的区块,还包括分支链上的区块。这里有个容易混淆的概念,人们通常所说的区块链实际上是区块链中的最长链,而真正的区块链是树状结构,这棵树上包含很多个分支。
区块索引存储在leveldb中,其中key以字符”b”开头,key/value格式为:’b’+32字节的区块hash值 -> 区块索引。每个区块索引存储如下内容:
1 2 3 4 5 |
|
UTXO(chainstate leveldb)
UTXO(Unspent Transaction Out):即未花费交易输出,通俗的讲指的就是比特币中的“币”。在比特币中并没有采用更直观的账户余额模型(每个账号对应相应的金额),而是采用UTXO模型,每个账号所拥有的比特币数量是该账号所有未花费交易输出的总和。当需要获取某个账号拥有的比特币数量时,就需要遍历所有未花费交易输出,进行累加。
UTXO在leveldb中的key/value格式:’c’+32字节的交易的hash->value。value中存储的内容如下:
1 2 3 4 5 |
|
rev*.dat
rev*.dat文件中存储的是未交易数据,这些未交易数据是区块进行回滚时所必须的。从本质上来讲,这些未交易数据其实是一系列CTxOut对象(该对象包含amount和scriptPubKey两个变量)
举个例子:假设有个交易A,它有三个交易输出:out1,out2,out3。当out1被花费时,out1中的amount和scriptPubKey会被写入到rev*.dat文件中。out2被花费时和out1类似。只有当out3被花费时,情况会有所不同。除了out3中的scriptPubKey和amount被写入以外,交易A的高度以及交易A是不是coinbase也需要被写入。
比特币序列化功能的实现都在streams.h和serialize.h这两个文件中。
首先我们通过一个demo来演示在比特币代码中如何进行序列化/反序列化操作
1 2 |
|
接下来我们来看看streams.h文件中的CDataStream类的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
CDataStream类中的vch对象是一个vector容器,用来存放序列化后的数据。write/read函数分别是向vch中写入数据和读数据。CDataStream类中重载了”<<“运算符,在该函数中调用了全局的Serialize方法。Serialize定义在serialize.h文件中。
serialize.h包含了一系列的Serialize的重载函数,包括signed和unsigned char,short,int,long,long long,char,float,double,bool以及string,vector,pair,map,set和CScript。根据传参的类型选择不同的Serialize函数。
下面以int32_t为例,分析调用的过程
1 2 3 4 5 6 7 |
|
::Serialize()-> Serialize()->s.write(),其中s为CDataStream对象,也就是说最后调用CDataStream中的write,把需要序列化的数据写入CDataStream的vch容器中。
以上的分析是对已知的数据类型进行序列化的过程。那么如果我们要序列化自定义类型的数据,该数据的类型和Serialize的重载函数中的类型都不匹配,那么该如何序列化呢?在serialize.h中有下面一种Serialize函数模板:
1 2 3 4 5 |
|
当要序列化自定义类型的数据和给定的类型不匹配时,就会调用该Serialize函数。在函数内部会调用对象a自身的Serialize函数。需要注意,对象a就是我们要序列化的对象,所以对于自定义类型的对象要想实现序列化,必须在类内部实现Serialize函数。
比特币中的任务调度,是个简单的生产者消费者模型。该模型通过一个条件变量,一个互斥锁以及一个消息队列来实现。
首先我们通过一个demo来学习条件变量和互斥锁的使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
|
以上代码中,消费者线程会先阻塞,等待生产者线程生产数据,之后通过cv.notify_one()唤醒阻塞的消费者线程。
比特币源码中的实现跟上面代码类似,但又有所不同。源码中定义了一个CScheduler类,该类中的成员变量taskQueue是multimap类型的任务队列,成员方法scheduleEvery,scheduleFromNow,schedule都是向taskQueue添加任务。与此同时,比特币在初始化的时候,创建了一个线程,这个线程专门负责从taskQueue中读取任务,然后执行。
在AppInit2中创建了一个线程,执行serviceLoop函数。
1 2 3 |
|
成员方法scheduleEvery,scheduleFromNow都是通过调用schedule实现了向taskQueue中添加任务,下面我们分析schedule的实现
1 2 3 4 5 6 7 8 |
|
在向taskQueue添加任务后,会调用newTaskScheduled.notify_one()唤醒任务队列中阻塞的一项任务。