月度归档:2018年04月

C++ 的左右值与左右值引用

左值与右值

C++ 中左值和右值的概念来源于 C, 在 C 中左值和右值的区别很简单, 能出现在赋值号左侧的就是左值, 否则就是右值. 比如变量是左值, 字面常量或者 const 定义的常量是右值.

然而在 C++ 中, 左值和右值的区别就不再是那么简单了, 甚至和 C 还会有冲突, 比如在 C 中 const 定义的常量对象是右值, 而在 C++ 中却是左值.

实际上在 C++ 中左值和右值的情况非常复杂, 有时区分他们也是非常困难的. Scott Meyers 大师在其 Effective Modern C++ 一书所说的不失为一个好方法, 在理解这句话之前, 我们一定要有一个意识, 就是左值和右值是表达式的属性, 代表着表达式的运算结果是左值还是右值.

A useful heuristic to determine whether an expression is an lvalue is to ask if you can take its address. If you can, it typically is. If you can’t, it’s usually an rvalue. A nice feature of this heuristic is that it helps you remember that the type of an expression is independent of whether the expression is an lvalue or an rvalue.

也就是说, 表达式是左值还是右值, 就看这个表达式的结果是否在内存有对应的存储区域, 有的话就是左右, 没有的话就是右值 (TODO: 补充例子), 这点也可以参考 c++ primer 5th, p121.

另外还有两个有重要的准则:

  1. 对象 (对象是最简单的表达式) 被用作左值时, 用的是对象的地址; 被用作右值时, 用的是对象的值
  2. 在需要右值的地方, 可以用左值代替 (因为地址中存储着值), 这时左值被当成右值使用, 用的是左值里存储的值. 这条准则只有一个例外, 就是左值引用不能当做右值引用使用 (下面会讲到引用)

引用

在 C++11 以前, 引用就是对象的别名, 只能绑定到左值上, 并且一旦绑定到了某个对象上就永远绑定了, 不能通过赋值改变引用绑定的对象, 因此在定义引用时必须初始化.

定义引用绑定到对象时, 一般是要求所绑定到的对象的类型和引用的类型必须一致, 不过也有两个例外:

  1. 定义的基类引用可以用派生类对象初始化 (c++ primer 5th, p534)
  2. 常量引用 (对 const 的引用) 可以用任意表达式作为其初始值, 只要该表达式的结果能转换成引用类型即可 (c++ primer 5th, p55)

上面说的引用只能绑定到左值上, 在 C++11 中扩展了引用的概念, 允许引用绑定到右值表达式这样的右值上, 这新引入的引用类型被称为右值引用, 对应的之前的引用被称为左值引用.

那么右值引用有什么用呢? 如果仅仅是让我们能够绑定到字面常量这样的右值的话, 刚刚我们也看到了, 通过常量(左值)引用我们同样也能够绑定到字面常量等右值表达式上, 为什么还要引入右值引用呢?

然而右值引用不仅仅是让我们能够不借助 const 就能引用右值表达式, 它还支持了对象移动以及完美转发这两个重要的能力. 关于这两个能力就是另外的话题了, 我们简单介绍一下对象移动.

对象移动

对象移动的目的是减少对象拷贝, 被移动的对象内容会被 “掏空”, 赋予新对象中, 这个操作依赖于 “移动构造函数” 或者 “移动操作符”, 然而如何定义这两个方法却成了一个问题, 我们知道拷贝构造函数和赋值操作符是以对象的引用 (左值引用) 为参数的, 而如果要定义移动构造函数或移动操作符, 其参数当然也得以代表对象的某种形态为参数, 对象的引用这种形态已经不能用了, 对象的非引用更不能用 (原因见 c++ primer 5th, p422), 那么只能新发明一中语义了, 这个新的语义就是右值引用, 写作 T &&. 于是移动构造函数就可以这么定义了:

Foo (Foo && other);

如此一来, 只要传递给构造函数的是右值引用, 就会调用移动构造函数, 免去拷贝所造成的开销.

然而如何传递右值引用呢? 当发生如下情况时, 该调哪个构造函数又成了一个问题:

Foo (const Foo &other);
Foo (Foo && other);

Foo(foo);    // 该调哪个构造函数?

事实上由于拷贝构造函数先出现, 所以上面第 4 行的写法当然是会调拷贝构造函数的. 为了能够调到移动构造函数, 标准库提供了一个 std::move 方法, 这个方法总是返回右值引用, 因此我们只要这么写就能够调用移动构造函数了:

Foo(std::move(foo));

除了显式的通过 std::move 方法, 有些情况下编译器也能够自动判断是否可以调移动构造函数来降低开销, 比如编译器发现源对象是个临时对象.

关于右值引用带来的另一个功能完美转发就不再敖述, 可以参考文末链接 1 关于右值引用的提案.

参考

  1. 右值引用提案: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2004/n1690.html

Cifer

四月 29, 2018

关于模板

一定要合理使用模板, 一定要优先保证可读性…

关于指针

个人觉得, 能少用指针就少用, 优先级如下:

引用 > 智能指针 > 指针

关于 const

引入 const 有意义吗? 变量是否应该改变应该由代码确保, 而不需要编译器确保. 相比与 const 带来的安全性, 它带来了更多的麻烦:

  1. const 类型的变量不能传入接受非 const 类型参数的函数, 这要求我们定义函数时参数类型最好都定义成 const 的. (c++ primer 5th, p192)

Cifer

四月 27, 2018

acceptor 一般使用方法:

using boost::asio::ip::tcp;

boost::asio::io_context     io_context;
boost::system::error_code   ec;

tcp::acceptor   acceptor(io_context)
tcp::socket     socket(io_context);
tcp::endpoint   ep_listen(tcp::v4(), 13000);
tcp::endpoint   ep_peer;

acceptor.open(ep.protocol(), ec);
acceptor.bind(ep, ec);
acceptor.listen(5, ec);
acceptor.accept(socket, ep_peer, ec);

...

这其实是有些繁琐的, 实际上 acceptor 的构造函数还有这个:

basic_socket_acceptor(
    boost::asio::io_context & io_context,
    const endpoint_type & endpoint,
    bool reuse_addr = true);

这里我们可以传入一个 endpoint 作为第二个参数, 提供了这个 endpoint 参数的话,
acceptor 会同时帮我们做 open, bind, listen 三件事,
所以上述代码可以写成如下这么短:

using boost::asio::ip::tcp;

boost::asio::io_context     io_context;
boost::system::error_code   ec;

tcp::endpoint   ep;
tcp::acceptor   acceptor(io_context, tcp::endpoint(tcp::v4(), 13000))
tcp::socket     socket(io_context);

acceptor.accept(socket, ep, ec);

...

Daniel Larmer 列传

  • 2013 年, 全职挺进区块链行业. 在 bitcointalk 上找到了 Adam Levine 和 Charles Hoskinson 一起开发了 bitshares
  • 2014 年发布 Bitshares 第一版, 沿用了比特币的数据库技术和 UTXO 思想, 导致性能不好, 不能跑什么实际应用
  • 2015, 首次提出, 从 “So how does Delegated Proof of Stake work?” 这段开始介绍了 DPoS 的机制
  • 2015 年 1 月, 发表对 Nothing at Stake 问题的看法: http://bytemaster.github.io/article/2015/01/08/Nothing-at-Stake-Nothing-to-Fear/
  • 2015 年 6 月, 发布了石墨烯工具组, 在此基础上建立 Bitshares 2.0, 这个架构的一个特点是东西全 load 进内存, 出块时间 3s
  • 2016 以前, 在 http://bytemaster.github.io/ 上发文, 后来在 steemit 上, 再后来在 medium 上.
  • 2017 年, dan 发表文章称 PoW 没有解决拜占庭将军问题 (实际也确实如此), https://steemit.com/blockchain/@dantheman/the-problem-with-byzantine-generals
  • 2018 年 1 月, dan 录了两段视频介绍 DPoS/PBFT: https://twitter.com/go_eos/status/956607047449137153, 网上有文字翻译.
  • 2018 年 2 月, Ivan 对 BM 做了一次采访, 内容很全, 讲述了自己的生平, bitshares/steem 的创建, 还讨论了以太坊和 EOS, 还有 DPoS 的起源: https://twitter.com/IvanOnTech/status/960908134628896768. 网上能找到中文文字版.
  • 2018 年 4 月, 巨蟹在巴比特直播, 表示如果时光倒流, 希望 BM 不要离开 http://www.8btc.com/20180412
  • 2018 年 5 月 5 日, 有媒体对巨蟹做了一次独家专访, 谈到了巨蟹的公开市场操作 https://mp.weixin.qq.com/s/h3twkhoOSmGRv1aBJZ0SfQ

    以上信息来源于互联网

Graphene 源码阅读 – RPC 篇 – 通信协议与服务端实现

从现在开始我们进入一个新的篇章: RPC 篇, 这个篇章会包含客户端 (钱包, UI 等) 与节点间的通信细则, API 分类, 服务端的实现等内容, 最后会挑几个主要的 API 讲一下.

本文是 RPC 篇的第一章, 我们就先来介绍一下整体的通信机制和服务端 (节点) 实现.

通信协议

Bitshares 提供两种通信方式: http 和 websocket. 这俩最大的区别就是 websocket 是双向通信, 客户端和服务段都能主动向对方发送消息; 而 http 则只能由客户端主动发送消息. Websocket 的双向通信特性能够满足一些对实时性需求较高的应用.

Websocket 和 http 如此不同, 但却又难解难分, websocket 是在 http 之后出现的, 它复用了 http 的传输层以及协议端口, 并且握手过程也是使用 http 消息格式实现的, 只不过在 http 头部添加了几个新的 headers, 当服务端检测到 websocket 的 headers 时, 就会知道这是个 websocket 连接, 从而与传统的 http 请求过程区分开来.

刚说了 websocket 复用了 http 的传输层, http 的传输层可以是未加密的 tcp, 也可以是加密过的 tls, 那么 websocket 自然也可以用这两种传输层协议.

\ http websocket
tcp http:// ws://
tls https:// wss://

关于 websocket 协议的细则可以自行 google 一下, 这里就不再敖述了.

消息格式

不管是 websocket 还是 http, 客户端与节点通信时的消息体都是 json 格式, 一个典型的请求体内容如下:

{
 "jsonrpc": "2.0",
 "id": 1
 "method": "get_accounts",
 "params": [["1.2.0", "1.2.1"]],
}

其中 id 是自增的, jsonrpc 目前固定是 2.0. methodparams 不用过多解释一看便知其意. 返回体会因请求不同而不同, 但当然也是标准的 json 格式, 而且一般会包含 errno, errmsg 这样的通用字段, 不再贴出.

服务端实现

服务端的实现借助了 websocketapp 库, 这个库能够帮助我们方便的开发 websocket 服务端程序, 不但如此, 它也支持对普通 http 消息的处理, 因为前面说了 websocket 和 http 使用共同的传输层和端口, websocket 协议也只是在握手阶段使用 http 的消息格式, 所以 websocketapp 很容易区分客户端发来的是 websocket 消息还是普通的 http 消息, 相应的做不同的处理, 为此 websocketapp 提供了两个回调接口: on_message 和 on_http, 应用程序可以注册这两个回调方法. 当收到 websocket 消息时, on_message 会被调用; 而收到普通 http 消息时, on_http 会被调用.

除了 on_message 和 on_http 之外还有一个重要的回调是 on_connection, 它代表着有新的客户端连接过来.

Bitshares 代码中当然是实现了这三个回调的, 下面我们就来看一下服务端启动的主要流程以及对上面三个回调的实现.

服务流程

在节点启动时, 会调用 application::startup() 方法, 而这个方法的最后一个工作就是启动 RPC server, 这在 reset_websocket_server() 方法中去做:

// 代码 1.1

 277 void application_impl::reset_websocket_server()
 278 { try {
 279    if( !_options->count("rpc-endpoint") )
 280       return;
 281
 282    _websocket_server = std::make_shared<fc::http::websocket_server>();
 283    _websocket_server->on_connection( std::bind(&application_impl::new_connection, this, std::placeholders::_1) );
 284
 285    ilog("Configured websocket rpc to listen on ${ip}", ("ip",_options->at("rpc-endpoint").as<string>()));
 286    _websocket_server->listen( fc::ip::endpoint::from_string(_options->at("rpc-endpoint").as<string>()) );
 287    _websocket_server->start_accept();
 288 } FC_CAPTURE_AND_RETHROW() }

这个方法很简单, 首先直接实例化了 _websocket_server 对象, 这个对象的类型是 fc::http::websocket_server, 它又是属于 fc 库的一部分, 然而这不重要, 这里不需要再了解 fc 库中对应的代码了. 实际上 fc::http::websocket_server 就是对前面我们说的 websocketapp 库的封装, 我们可以把 fc::http::websocket_server 就看做是 websocketapp.

那么可以看到紧接着就是注册了 on_connection 回调, 然后就是 listen, accept, 多么熟悉的套接字编程套路, websocket 服务端就这么愉快的启起来了~

我知道你要问什么, 怎么没看见注册 on_message 和 on_http 回调呢? 对了, 看到注册 on_connection 回调用的 application_impl::new_connection 方法了吗, on_message 和 on_http 实际上就是在这个方法里注册的:

// 代码 1.2

 245 void application_impl::new_connection( const fc::http::websocket_connection_ptr& c )
 246 {
 247    auto wsc = std::make_shared<fc::rpc::websocket_api_connection>(*c, GRAPHENE_NET_MAX_NESTED_OBJECTS);
 248    auto login = std::make_shared<graphene::app::login_api>( std::ref(*_self) );
 249    login->enable_api("database_api");
 250
 251    wsc->register_api(login->database());
 252    wsc->register_api(fc::api<graphene::app::login_api>(login));
 253    c->set_session_data( wsc );
….
….

// 代码 1.3

 10 websocket_api_connection::websocket_api_connection( fc::http::websocket_connection& c, uint32_t max_depth )
 11    : api_connection(max_depth),_connection(c)
 12 {
 13    _rpc_state.add_method( "call", [this]( const variants& args ) -> variant
 14    {
 15       FC_ASSERT( args.size() == 3 && args[2].is_array() );
…
…
…
 48    } );
 49
 50    _connection.on_message_handler( [&]( const std::string& msg ){ on_message(msg,true); } );
 51    _connection.on_http_handler( [&]( const std::string& msg ){ return on_message(msg,false); } );
 52    _connection.closed.connect( [this](){ closed(); } );
 53 }

application_impl::new_connection 的参数 fc::http::websocket_connection_ptr 这个类型又是对 websocketapp 的封装, 不难理解, 我们直接认为它就是 websocketapp 传过来的对这个新连接的上下文描述就好, 紧接着实例化了一个 fc::rpc::websocket_api_connection 对象并且把这个上下文传了进去, fc::rpc::websocket_api_connection 的构造函数在代码 1.3, 可以看到在构造函数最后它注册了 on_message 和 on_http 的 handler, 而这两个 handlers 实际上是调用的同一个方法: on_message, 注意这里这个 on_message 可是 websocket_api_connection::on_message.

到这里为止, 我们就知道该如何 track 各种请求在服务期短的处理了, 新连接的处理就看 application_impl::new_connection, 来了请求怎么处理就看 websocket_api_connection::on_message, 当然对 websocket 来说还有一个过程就是服务器端主动发消息给客户端的过程, 这部分感兴趣可以自己研究一下, 提示一下: fc::http::websocket_connection::send_message 方法.

后记

本文最后引出了 on_connection 和 on_message 这两个重要的回调, 下篇文章将会简单介绍 on_connection 实现, 然后从 on_message 展开介绍一下各类 api 们, 以及从请求体到这些 api 们的映射机制.

Graphene 源码阅读 – 番外篇 – 出块判断逻辑

见证人节点起来之后, 会周期行的检查是否轮到自己出块了, 判断自己是否能出块的逻辑在 maybe_produce_block()方法中, 这里面的第一步就是判断自己是不是和网络同步了, 同步了的话就继续下面的判断, 没有同步的话就直接返回 not_synced 错误告诉外面不要出块.

211 block_production_condition::block_production_condition_enum witness_plugin::maybe_produce_block( fc::limited_mutable_variant_object& capture )
212 {
213    chain::database& db = database();
214    fc::time_point now_fine = fc::time_point::now();
215    fc::time_point_sec now = now_fine + fc::microseconds( 500000 );
216
217    // If the next block production opportunity is in the present or future, we're synced.
218    if( !_production_enabled )
219    {
220       if( db.get_slot_time(1) >= now )
221          _production_enabled = true;
222       else
223          return block_production_condition::not_synced;
224    }

正常情况下, 第一次启动时 _production_enabled 的值是 false, 因此这段逻辑确保了在同步到最新块之前本节点肯定不会出块的, 否则就分叉了; 而一旦达到过一次同步状态, _production_enabled 就会置为 true, 我搜索了全部代码, 程序运行周期中没有其它地方会将 _production_enabled 再置为 false 了, 而且 maybe_produce_block() 方法中后面也没有对节点同步状态进行检查的逻辑了, 这就意味着只要见证人节点曾经同步到最新过, 并且始终没有宕机没有重启, 那么就算后来节点不再处于同步状态, 照样也能出块?

这个行为我诈一看感觉不太合理, 按理说节点如果没同步的话那肯定也得直接返回 not_synced 不能让出块呀! 所以 maybe_produce_block() 这段逻辑得改改, 于是我计划写个 issue 然后提个 patch, 修改方案已经想好了, head_block_time() + block_interval()now 比较, 如果前者大就说明节点已经同步到最新状态了, 否则就说明节点显然已经落后了 (这也是上述代码中 db.get_slot_time() 方法的逻辑). 这样一来上面这段代码可以改成类似如下这样就可以了:

211 block_production_condition::block_production_condition_enum witness_plugin::maybe_produce_block( fc::limited_mutable_variant_object& capture )
212 {
213    chain::database& db = database();
214    fc::time_point now_fine = fc::time_point::now();
215    fc::time_point_sec now = now_fine + fc::microseconds( 500000 );
216
217    // If the next block production opportunity is in the present or future, we're synced.
218    if( db.get_slot_time(1) < now )
219       return block_production_condition::not_synced;

本来 issue 都已经写完了, 然而在写完的时候我才意识到自己错了, 如果按照上面的说法做的话, 可能会导致从某个时间起所有见证人都不再出块, 用一个简单的例子说明. 假设有 A, B, C 三个见证人, 他们先按照 A -> B -> C 的顺序每人出了一个块, 然后顺序变成 B -> A -> C, 所以这时轮到 B 出块了, 然而不幸的是 B miss 了, 于是到 A, A 这时判断最新块的时间加上出块间隔发现小于当前时间, 按照上面的策略, 这个认为自己 out of sync 了, 所以 A 也不会出块, 到 C 时也是一样的情况, C 也认为自己 out of sync 于是 C 也不出块. 于是.. 就达到了一个没有节点出块的局面…

所以说 _production_enabled 实际上正是避免了出现这个局面, 只是它的名字可能有点让人困惑, 实际上它就是一个标识, 标识着当节点第一次达到了与全网同步的状态时, 它就初步具备了出块资格.

也许改叫 once-synced 之类的名字会好些 😛

(PS: 这篇的内容貌似已经有点涉及 DPoS 啦)

Graphene 源码阅读 – 数据库篇 – 区块数据管理

在 bitshares 中除了对象数据需要落盘之外, 网络上接收到的区块数据也是需要落盘的, 区块数据的管理依赖的就是 chain::block_database 模块.

chain::block_database

这个模块听起来好像很复杂 – 毕竟管理着区块数据的落盘与加载, 但实际上非常简单. 它维护了两个文件流: _blocks 存储具体区块数据, _block_num_to_pos 则是索引数据. 当我们知道了区块 id 或者区块号时, 就可以从 _block_num_to_pos 查询出此区块在 _blocks 中的偏移和大小, 进而从 _blocks 中取出整个块数据.

区块数据的内存模型

区块数据在内存中由 chain::signed_block 类型表示, 但是与对象在内存中会挂到红黑树上不同, 区块数据从其他节点收到并 apply 完后就会序列化到磁盘并在内存中释放.

区块数据与区块索引的序列化与反序列化

读写 _blocks 文件流时会涉及到区块数据的序列化与反序列化, 这里依赖的也是 对象序列化 中介绍的 fc::raw::pack/fc::raw::unpack, 不再敖述.

而读写 _block_num_to_pos 流时关于区块索引的序列化反序列化就简单了, 它没有使用 fc::raw::pack/fc::raw::unpack, 而是简单粗暴的强转成字符序列:

// 代码 6.1

 78 void block_database::store( const block_id_type& _id, const signed_block& b )
 79 {
 80    block_id_type id = _id;
 81    if( id == block_id_type() )
 82    {
 83       id = b.id();
 84       elog( "id argument of block_database::store() was not initialized for block ${id}", ("id", id) );
 85    }
 86    _block_num_to_pos.seekp( sizeof( index_entry ) * int64_t(block_header::num_from_id(id)) );
 87    index_entry e;
 88    _blocks.seekp( 0, _blocks.end );
 89    auto vec = fc::raw::pack( b );
 90    e.block_pos  = _blocks.tellp();
 91    e.block_size = vec.size();
 92    e.block_id   = id;
 93    _blocks.write( vec.data(), vec.size() );
 94    _block_num_to_pos.write( (char*)&e, sizeof(e) );
 95 }

反序列过程实际上面说过了, 就是拿区块 id 或者区块号从 _block_num_to_pos 查询出此区块在 _blocks 中的偏移和大小, 然后从 _blocks 中取出整个块数据:

// 代码 6.2

184       index_entry e;
185       int64_t index_pos = sizeof(e) * int64_t(block_num);
186       _block_num_to_pos.seekg( 0, _block_num_to_pos.end );
187       if ( _block_num_to_pos.tellg() <= index_pos )
188          return {};
189
190       _block_num_to_pos.seekg( index_pos, _block_num_to_pos.beg );
191       _block_num_to_pos.read( (char*)&e, sizeof(e) );
192
193       vector<char> data( e.block_size );
194       _blocks.seekg( e.block_pos );
195       _blocks.read( data.data(), e.block_size );
196       auto result = fc::raw::unpack<signed_block>(data);
197       FC_ASSERT( result.id() == e.block_id );
198       return result;

chain::database 初始化

如果说 db::object_database 是管理对象数据, chain::block_database 管理区块数据, 那么 chain::database 就是管理 db::object_databasechain::block_database 了. 从结构上来讲 chain::database 的内容应该单独放在一篇文章里, 但是由于 block_database 模块比较简单, 篇幅太少, 而 chain::database 的内容有比较多, 估计下一篇一篇介绍不完, 所以这里就暂且先把 chain::database 的初始化部分挪到这里来. 也能让我们先对 chain::database 有个大概的认识.

chain::database 的初始化涉及到两个过程, 一个是节点实例被构造时, 同时也会构造 chain::database 实例, 而在 chain::database 构造时就会执行我们前面章节提到过的 initialize_indexes(), add_index<>() 等过程. 对应下面这条链路:

node = new app::application() => new detail::application_impl(this) => _chain_db(std::make_shared<chain::database>()

chain::database() => initialize_indexes()
                 => add_index<>()

其次就是在节点 startup 时, 会调用 chain::database::open() 方法, 这里面包含了 chain::database 初始化阶段的主要工作内容.

首先, 读取 witness_node_data_dir/blockchain/db_version 文件, 比较一下数据库版本和当前运行的版本的程序的数据库版本是否一致, 如果版本不一致或者这个文件不存在, 就会先清空对象库, 然后写入当前的版本号.

// 代码 6.3
143       bool wipe_object_db = false;
144       if( !fc::exists( data_dir / "db_version" ) )
145          wipe_object_db = true;
146       else
147       {
148          std::string version_string;
149          fc::read_file_contents( data_dir / "db_version", version_string );
150          wipe_object_db = ( version_string != db_version );
151       }
152       if( wipe_object_db ) {
153           ilog("Wiping object_database due to missing or wrong version");
154           object_database::wipe( data_dir );
155           std::ofstream version_file( (data_dir / "db_version").generic_string().c_str(),
156                                       std::ios::out | std::ios::binary | std::ios::trunc );
157           version_file.write( db_version.c_str(), db_version.size() );
158           version_file.close();
159       }

然后就是调用 object_databaseblock_database 的 open() 方法, 到这里我们看到了 chain::database 确实是操控 object_databaseblock_database 的 “上游” 模块.

// 代码 6.4

161       object_database::open(data_dir);
162
163       _block_id_to_block.open(data_dir / "database" / "block_num_to_block");

接下来, 如果这是节点的第一次启动 (说明从 object_database 中加载的对象中没有找到 global_property_object) 的话, 就要初始化创世状态, 而创世信息从里来呢? 请参考 Genesis 创世信息生成.

// 代码 6.5

165       if( !find(global_property_id_type()) )
166          init_genesis(genesis_loader());

创世过程的主要工作包括创建一些特殊和初始账户, 以及一些核心资产, 这些信息不用经过广播直接在本地出块, 因为所有其他节点也要执行要全同样的过程.

指的一提的是, 创世信息中的账户我本以为都是链上的公共账户或者委员会特殊账户之类的, 但没想到里面还有 9w 多的正常会员账户, 这些会员账户可谓是 bitshares 共链的 “创世居民”.

后记

区块中保存了整条链的原始记录, 只要区块数据正确完好, 我们就能从这些数据中 replay 出一条一模一样的链.

好了, 本文就到此为止. 感谢阅读.