CDataStream

本系列博文采用的比特币源码版本是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加锁/开启RPC1.给存放数据的目录加锁,确保只有一个比特币进程使用该数据目录

 

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

* 区块头信息

* 区块的高度

* 区块中的交易数量

* 区块存储在哪个blk*.dat文件中,以及区块在该文件中的偏移量

* 未交易数据存储在哪个rev*.dat文件中,以及未交易数据在该文件中的偏移量

 

UTXO(chainstate leveldb)
UTXO(Unspent Transaction Out):即未花费交易输出,通俗的讲指的就是比特币中的“币”。在比特币中并没有采用更直观的账户余额模型(每个账号对应相应的金额),而是采用UTXO模型,每个账号所拥有的比特币数量是该账号所有未花费交易输出的总和。当需要获取某个账号拥有的比特币数量时,就需要遍历所有未花费交易输出,进行累加。

UTXO在leveldb中的key/value格式:’c’+32字节的交易的hash->value。value中存储的内容如下:

1

2

3

4

5

*交易的版本

*这个交易是不是coinbase

*哪个高度的区块包含这个交易

*这个交易的第几个output是未花费的

*未花费交易输出的scriptPubKey和amount

 

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

CDataStream ss(SER_GETHASH,0);  //实例化CDataStream对象

ss<<obj1; //序列化 ss>>obj2; //反序列化 

接下来我们来看看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

class CDataStream

{

protected:

    typedef CSerializeData vector_type;

    vector_type vch;

    unsigned int nReadPos;

public:

    ...

    CDataStream& read(char* pch, size_t nSize)

    CDataStream& write(const char* pch, size_t nSize)

    {

        // Write to the end of the buffer

        vch.insert(vch.end(), pch, pch + nSize);

        return (*this);

    }

    template<typename T>

    CDataStream& operator<<(const T& obj)

    {

        // Serialize to this stream

        ::Serialize(*this, obj, nType, nVersion);

        return (*this);

    }

 

    template<typename T>

    CDataStream& operator>>(T& obj)

    {

        // Unserialize from this stream

        ::Unserialize(*this, obj, nType, nVersion);

        return (*this);

    }

    ...

}

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

template<typename Stream> inline void Serialize(Stream& s, int32_t a,      int, int=0) { ser_writedata32(s, a); }

 

template<typename Stream> inline void ser_writedata32(Stream &s, uint32_t obj)

{

    obj = htole32(obj);

    s.write((char*)&obj, 4);

}

::Serialize()-> Serialize()->s.write(),其中s为CDataStream对象,也就是说最后调用CDataStream中的write,把需要序列化的数据写入CDataStream的vch容器中。

以上的分析是对已知的数据类型进行序列化的过程。那么如果我们要序列化自定义类型的数据,该数据的类型和Serialize的重载函数中的类型都不匹配,那么该如何序列化呢?在serialize.h中有下面一种Serialize函数模板:

1

2

3

4

5

template<typename Stream, typename T>

inline void Serialize(Stream& os, const T& a, long nType, int nVersion)

{

    a.Serialize(os, (int)nType, nVersion);

}

当要序列化自定义类型的数据和给定的类型不匹配时,就会调用该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

boost::mutex mutex;

boost::condition_variable cv;

std::string data;

bool ready = false// 条件

bool processed = false// 条件

  

void Worker() {

    boost::unique_lock<boost::mutex> lock(mutex);

  

    // 等待生产者线程发送数据。

    cv.wait(lock, [] { return ready; });

  

    // 等待后,继续拥有锁。

    std::cout << "消费者线程正在处理数据..." << std::endl;

    // 睡眠一秒以模拟数据处理。

    boost::this_thread::sleep_for(boost::chrono::seconds(1));

    data += " 已处理";

  

    // 把数据发回生产者线程。

    processed = true;

    std::cout << "消费者线程通知数据已经处理完毕。" << std::endl;

  

    // 通知前,手动解锁以防正在等待的线程被唤醒后又立即被阻塞。

    //lock.unlock();

  

    cv.notify_one();

}

  

int main() {

    boost::thread worker(Worker);

  

    // 把数据发送给消费者线程。

    {

        boost::lock_guard<boost::mutex> lock(mutex);

        std::cout << "生产者线程正在准备数据..." << std::endl;

        // 睡眠一秒以模拟数据准备。

        boost::this_thread::sleep_for(boost::chrono::seconds(1));

        data = "样本数据";

        ready = true;

        std::cout << "生产者线程通知数据已经准备完毕。" << std::endl;

    }

    cv.notify_one();

  

    // 等待消费者线程处理数据。

    {

        boost::unique_lock<boost::mutex> lock(mutex);

        cv.wait(lock, [] { return processed; });

    }

    std::cout << "回到生产者线程,数据 = " << data << std::endl;

  

    worker.join();

  

    return 0;

}

以上代码中,消费者线程会先阻塞,等待生产者线程生产数据,之后通过cv.notify_one()唤醒阻塞的消费者线程。

比特币源码中的实现跟上面代码类似,但又有所不同。源码中定义了一个CScheduler类,该类中的成员变量taskQueue是multimap类型的任务队列,成员方法scheduleEvery,scheduleFromNow,schedule都是向taskQueue添加任务。与此同时,比特币在初始化的时候,创建了一个线程,这个线程专门负责从taskQueue中读取任务,然后执行。

在AppInit2中创建了一个线程,执行serviceLoop函数。

1

2

3

// Start the lightweight task scheduler thread

CScheduler::Function serviceLoop = boost::bind(&CScheduler::serviceQueue, &scheduler);

threadGroup.create_thread(boost::bind(&TraceThread<CScheduler::Function>, "scheduler", serviceLoop));

成员方法scheduleEvery,scheduleFromNow都是通过调用schedule实现了向taskQueue中添加任务,下面我们分析schedule的实现

1

2

3

4

5

6

7

8

void CScheduler::schedule(CScheduler::Function f, boost::chrono::system_clock::time_point t)

{

    {

        boost::unique_lock<boost::mutex> lock(newTaskMutex);

        taskQueue.insert(std::make_pair(t, f));

    }

    newTaskScheduled.notify_one();

}

在向taskQueue添加任务后,会调用newTaskScheduled.notify_one()唤醒任务队列中阻塞的一项任务。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值