#区块链#Bitshares学习笔记:节点启动浅析

前言

最近准备学习区块链的底层技术,打算以Bitshares公链为学习例子。为什么要选择 Bitshares 呢?主要是因为自己接触的第一个区块链就是 Bitshares ,而它跟目前很火的公链 EOS 以及公信宝(GXB)都是用一个叫 Graphene 的底层工具库开发的,同属 BM 的杰作。另外 Graphene 作为高性能区块链工具库催生了很多优秀的区块链项目,以 Bitshares 为切入点去了解 Graphene 也是相当不错的。在学习之余,分享一下自己的学习笔记,一方面希望能为社区做一点贡献,另一个方面是希望有大佬能在小编理解错时指点一下,小编目前对c++还不熟悉,可能犯很多低级错误,望见谅。


适合什么人看

整个学习流程小编只会从比较宏观的角度去分析一个区块链程序是怎样跑起来的,其中涉及什么模块,模块之间又是如何交织支撑起区块链运行的。适合对区块链行业感兴趣但还是持着观望态度的程序员,希望这些受众在看完整个系列后,获得的知识能够帮助个人在合适的时机转到区块链行业。


本章主要是讲节点启动流程,以及涉及的数据库和网络模块分析。

代码地址:https://github.com/bitshares/bitshares-core


首先看一下witness_node的程序入口(对于不必要的代码都隐藏了,用备注来代替)


文件programs/witness_node/main.cpp
int main(int argc, char** argv) {
      app::application* node = new app::application();
      // 读取命令行带上的配置项
      // 加载插件,后面分析插件机制
      // 解析命令行指令
      // 加载本地config.ini配置文件  ps:命令行带上的配置项优先级比config.ini配置的要高
      // 节点程序初始化(主要是查看需要加载哪些插件)
      // 节点插件初始化

      // 节点程序启动
      node->startup();
      // 节点程序插件启动
      // 收到ctrl+c命令后关闭节点程序,进行关闭前的数据保存操作
      return 0;
}

转到node->startup()内部

文件libraries/app/application.cpp
void startup()
{ 
   // 创建保存区块数据的目录
   auto initial_state = // 初始化创世块内容的匿名函数

   // 打开数据库
      _chain_db->open( _data_dir / "blockchain", initial_state, GRAPHENE_CURRENT_DB_VERSION );

   // api访问权限设置读取
   // 重置p2p网络状态
   reset_p2p_node(_data_dir);
   // 重置websocket服务状态
}


这里我们重点关注一下数据库和p2p的启动过程,毕竟这两个是区块链骨架的核心。


先从数据库着手:(个人习惯带着问题或猜想去阅读源码,这样更加有目的性和趣味性,而不是单纯的看和记)

问题1:既然区块链的数据是以一个个连续的区块来保存,那么如果要查询一个用户的余额,要怎么查询,这个数据存在了哪?


数据库启动过程:

文件libraries/chain/db_management.cpp
void database::open(
   const fc::path& data_dir,
   std::function<genesis_state_type()> genesis_loader,
   const std::string& db_version)
{
      // 检查版本
      // 打开 object 数据库(为了能避免混淆使用英文名来说明)
      object_database::open(data_dir);
      // 打开 block 数据库( object 数据库和 block 数据库是有区别的)
      _block_id_to_block.open(data_dir / "database" / "block_num_to_block");
      // 如果数据为空,则用genesis_loader创世块配置初始化数据库
      // 验证 block 数据库最新的区块是否与 object 数据库一致,不一致则将没记录的区块处理并更新 object 数据库
      reindex( data_dir );
}

可以看出数据库分为两个部分,一个是 block(区块)数据库,存储的是每一个区块的原始数据;另一个是 object(对象)数据库,它是从 block 数据库解析每一个区块数据后得出的区块链中各个对象当前状态的数据库。打个比方,区块1包含创建用户b,区块2包含用户b作为见证人获得10bts, object 数据库解析了区块1之后,用户列表多了一个用户b,解析区块2后状态变成用户b存款有10bts。


用一个表达式来表示:

parse(nextblock, state) = nextstate


object 数据库的启动过程:

文件libraries/db/object_database.cpp
void object_database::open(const fc::path& data_dir)
{ 
   // 读取不同object的数据,bts中不同的数据类型都会有对应的object_id
   // 格式是x.x.x,分别代表spaceID,typeID,index,spaceID的区别还没明白,typeID就是区分不同对象类型,index就是同一对象类型不同实体
   // 例如1.3.前缀代表资产,1.2.前缀代表用户,1.2.99代表第99个用户,1.3.113代表第113个资产
   // 这里可以看出每个对象类型的数据都是由单独的文件保存的
   _index[space][type]->open( _data_dir / "object_database" / space/ type);
}

文件libraries/db/include/graphene/db/index.hpp
virtual void open( const path& db )override
{ 
   // 创建内存映射文件
   // 从内存映射文件反序列化数据并保存起来,具体序列号和反序列化的实现是在/libraries/fc/include/fc/io/raw.hpp
}

object 数据库为了避免每次启动都要重新解析所有块来生成对象的状态,把状态都保存到了文件里,启动的时候再从文件中解析对象状态。


数据库模块的数据流向图:

除了区块数据库以外,节点还维护了对象数据库,用来保存数据对象的状态,我们查余额的时候实际上是查询对象数据库的内容,而不是直接从区块数据库查内容。


p2p启动流程:

问题1:如何在一开始的时候发现存在的并且有效的节点?(冷启动问题)


文件libraries/app/application.cpp
void reset_p2p_node(const fc::path& data_dir){ 
   _p2p_network = std::make_shared<net::node>("BitShares Reference Implementation");

   // 加载配置
   _p2p_network->set_node_delegate(this);

   // 如果没有指定p2p节点则使用默认节点
   vector<string> seeds = {
      "104.236.144.84:1777",               // puppies      (USA)
      "bts-seed1.abit-more.com:62015",     // abit         (China)
       ...省略很多
   };
   // 添加节点到_p2p_network中

   // 配置项读取
   // 监听p2p网络,等于向外提供p2p服务
   _p2p_network->listen_to_p2p_network();
   // 连接p2p网络
   _p2p_network->connect_to_p2p_network();
   // 从p2p网络中同步区块数据
   _p2p_network->sync_from(net::item_id(net::core_message_type_enum::block_message_type,
                                        _chain_db->head_block_id()),
                           std::vector<uint32_t>());
}

原来冷启动连接的p2p节点是写死的,域名形式的节点可能被墙,ip形式的可能不稳定,两者都有刚好互补。


连接到p2p网络的操作有很多(为了阅读体验,只保留了要说明的代码,其余的都用一句注释来概括了)

文件/bitshares-core/libraries/net/node.cpp
void node_impl::connect_to_p2p_network(){
   // 循环监听是否有p2p连接请求,为了减轻dos攻击影响,设置10毫米间隔
   // 循环连接,连接20个节点后进入10秒睡眠,不断重复该过程
   // 向其他节点请求同步区块
   _fetch_sync_items_loop_done = fc::async( [=]() { fetch_sync_items_loop(); }, "fetch_sync_items_loop" );
   // 向其他节点请求最新的数据
   _fetch_item_loop_done = fc::async( [=]() { fetch_items_loop(); }, "fetch_items_loop" );
   // 将节点收到的交易广播到区块链网络中
  _advertise_inventory_loop_done = fc::async( [=]() { advertise_inventory_loop(); }, "advertise_inventory_loop" );
   // 循环关闭超时没响应的节点,向其他有效节点发送心跳包
   // 循环请求当前p2p网络节点信息,15分钟一次
   // 统计网络下载和上传速率,每秒一次
   // 节点的状态log,每分钟一次
 }
}

网络连接时创建了很多定时任务来维护p2p网络和保证数据同步,其中fetch_sync_items_loop、fetch_items_loop、advertise_inventory_loop这三个任务跟区块链的业务逻辑比较相关,分别是用于从其他节点同步区块数据、从其他节点获取区块数据、广播数据到其他节点。


看到这里,大致知道了节点启动时是走了怎么样的流程,涉及了什么:

1.启动过程主要涉及数据库加载和p2p网络加载

2.数据库分为区块数据库和对象数据库,对象数据库是通过解析区块数据库得出的

3.p2p网络模块主要负责维护p2p网络和保证数据同步


那么Bitshares是怎么校验数据的正确性?数据不正确的时候怎么处理?

这个下一篇再讲了,脑壳疼,小编很少写文章,会存在很多自身无法发现的问题条,希望各位路过的大佬多多斧正,谢谢~


猜你喜欢

转载自juejin.im/post/5b0e3ca9f265da092a2b77bf