前言node
最近准备学习区块链的底层技术,打算以Bitshares公链为学习例子。为何要选择 Bitshares 呢?主要是由于本身接触的第一个区块链就是 Bitshares ,而它跟目前很火的公链 EOS 以及公信宝(GXB)都是用一个叫 Graphene 的底层工具库开发的,同属 BM 的杰做。另外 Graphene 做为高性能区块链工具库催生了不少优秀的区块链项目,以 Bitshares 为切入点去了解 Graphene 也是至关不错的。在学习之余,分享一下本身的学习笔记,一方面但愿能为社区作一点贡献,另外一个方面是但愿有大佬能在小编理解错时指点一下,小编目前对c++还不熟悉,可能犯不少低级错误,望见谅。c++
适合什么人看git
整个学习流程小编只会从比较宏观的角度去分析一个区块链程序是怎样跑起来的,其中涉及什么模块,模块之间又是如何交织支撑起区块链运行的。适合对区块链行业感兴趣但仍是持着观望态度的程序员,但愿这些受众在看完整个系列后,得到的知识可以帮助我的在合适的时机转到区块链行业。程序员
本章主要是讲节点启动流程,以及涉及的数据库和网络模块分析。github
代码地址:https://github.com/bitshares/bitshares-coreweb
首先看一下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()内部api
文件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的启动过程,毕竟这两个是区块链骨架的核心。bash
先从数据库着手:(我的习惯带着问题或猜测去阅读源码,这样更加有目的性和趣味性,而不是单纯的看和记)websocket
问题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是怎么校验数据的正确性?数据不正确的时候怎么处理?
这个下一篇再讲了,脑袋疼,小编不多写文章,会存在不少自身没法发现的问题条,但愿各位路过的大佬多多斧正,谢谢~
打个公众号广告~