您正在查看: 2019年6月

EOS区块生产和区块同步

1 概述

本文所述基于EOSv1.2.3。
EOS区块生产和同步主要涉及共识算法DPOS和aBFT,其源码实现主要涉及chain_plugin、producer_plugin、net_plugin和controller4个模块以及eosio.system智能合约等。

2 共识算法

EOS的区块生产,遵循DPoS(Delegated Proof-of-Stake)机制。
简单来说,所有拥有EOS token的人都是EOS区块生产的参与者。
任何人都可以申请出块。
任何人都可以选择不直接完成出块工作,而是将自己所持token抵押给出块申请者(PoS),委托(Delegate)他们完成出块工作。
最终,按照token比例,选出前21名出块者(BP,Block Producer),由他们代理出块。

ps:上述部分,EOS选举主要在eosio.system智能合约中实现,设置生产者队列函数为update_elected_producers()。笔者工作EOS不用于公链而是联盟链,不需要选举,故直接调用eosio.bios中的setprods()函数设置。

21名BP依次轮流出块,不像比特币等,同一时刻所有BP是竞争关系。每个BP轮到自己出块时,连续出块12个,每个块耗时500ms。

ps:上述这一块逻辑在producer_plugin中实现。

至此,我们可以简单的说,EOS的共识机制叫DPoS(More than that)。

EOS中每个区块被生产出来后,需要所有BP的确认,按照BFT共识机制确认后,才会变成不可逆的状态,在此之前都是reversible block。因此,需要进行区块在网络中的同步。
数据一致性是分布式系统数据同步的重要话题,BFT(Byzantine fault tolerance)是其中的一种代表共识机制,或者称它为算法。

如上图所示,BFT共识机制主要有以下步骤:

  • 提案1个block;
  • 进入Pre-commitment阶段,所有BP确认该提案;
  • 进入Commitment阶段,所有BP收到2/3+1或更多Pre-commit后,发送Commit;
  • 当某个BP收到2/3+1或更多Commit后,该block即不可逆。
  • 该算法适用于恶意节点不超过1/3的场景,可以保证达到最终一致性。
至此,我们可以说:EOS的共识机制叫DPoS & BFT。

BFT算法对每个区块需要发送至少2条消息,EOS对此进行了优化。BM将其称为pipelined:管道式、流水线式。
即每次生产一个区块,由于本身就需要将该区块广播到p2p网络中供其他节点同步,因此将Pre-Commit和Commit数据与该区块数据一并广播出去。
如下图所示:

上半部分描述了区块number不可逆的过程,下半部分描述了每个区块由谁Pre-Commit,由谁Commit。
假设有ABCD四个BP,每个BP每次出1个区块。BFT要求2/3+1个节点确认,即2/3*4+1=3。
假设每轮都是按照A、B、C、D的顺序出块。

  • A 出块1,Pre-Commit不可逆块为0,Commit不可逆块为0
  • B 出块2,Pre-Commit不可逆块为0,Commit不可逆块为0
  • C 出块3,ABC3个节点Pre-Commit区块1,故Pre-Commit不可逆块为1,Commit不可逆块为0
  • D 出块4,BCD3个节点Pre-Commit区块2,故Pre-Commit不可逆块为2,Commit不可逆块为0
  • A 出块5,CDA3个节点Pre-Commit区块3,故Pre-Commit不可逆块为3,CDA3个节点Commit区块1,故Commit不可逆块为1
  • B 出块6,DAB3个节点Pre-Commit区块4,故Pre-Commit不可逆块为4,DAB3个节点Commit区块2,故Commit不可逆块为2
  • 以此类推

基于上述优化,p2p网络的带宽压力、区块哈希验证频次等大幅降低。
但带来的问题是,出块、不可逆糅合在了一起,而非并行的。
BM的解释是:虽然如此,相对其它平台(比特币,以太坊)的机制来说,一个区块从生产出来到变为不可逆,其时间不算太久,且实际上每两个不可逆块之间的间隙时间非常短。长远考虑,只有这样,才能实现更好的扩展性。

原文地址:DPOS BFT— Pipelined Byzantine Fault Tolerance

至此,我们应当说:EOS的共识机制叫 DPOS BFT— Pipelined Byzantine Fault Tolerance。

3 chain_plugin

chain_plugin插件主要功能为:

  • 检查启动参数,判断是否需要replay区块链
  • 初始化和拉起Controller模块
  • 提供相关信号、方法,主要用于给controller、net_plugin、producer_plugin、用户输入等架桥
  • 向用户提供链的其它set/get接口,诸如:get_account、get_transaction_id……

下图为chain_plugin核心代码逻辑

3.1 检查启动参数,判断是否需要replay()区块链

chain_plugin启动前,先调用plugin_initialize()函数,该函数检查启动参数。除了一些基本配置外,还检查replay相关参数:

  • export-reversible-blocks:会将原来的reversible_block进行导出备份,该参数单独优先检查
  • delete-all-blocks:删除所有旧的blocks,重头开始replay区块链,包括清空State DB和Block log
  • hard-replay-blockchain:清空State DB,如果有配置truncate-at-block,按照[first,truncate-at-block]区间,否则[first,end]区间重新加载Block log中的区块,即该区间的区块仍然生效不replay,其它区块全部replay。如果reversible db有数据,一并尝试恢复
  • replay-blockchain:清空State DB,如果有配置fix-reversible_blocks,则尝试恢复reversible DB
  • fix-reversible_blocks:尝试恢复reversible DB
  • import-reversible_blocks:旧的不要,用导入的替代

3.2 初始化和拉起Controller模块

当plugin_initialize()上述操作处理完后,根据最终配置参数,对Controller模块进行初始化(emplace()-->构造函数),然后在调用plugin_startup()时调用了Controller::startup,完成了Controller模块的启动

3.3 提供相关信号、方法,主要用于给controller、net_plugin、producer_plugin、用户输入等架桥

  • accepted_block:当net_plugin收到一个区块,或者用户手动push的区块(可能仅仅是一个接口,该场景应当不常见),chain_plugin通过该方法转发区块到producer_plugin中
  • accepted-transactions:同理。需注意,cleos/http的push transaction和push action等,均在此处入链。

3.4 向用户提供链的其它set/get接口

实现了http接口中的一部分接口。

4 producer_plugin

producer_plugin插件主要功能为:

  • 当本节点处于生产阶段时,与chain_plugin::controller交互,调用controller相关接口进行区块生产;
  • 当本节点处于同步阶段时,接收net_plugin插件收到的区块,调用controller相关接口进行区块同步。

producer_plugin核心函数有:

  • schedule_production_loop:递归调用,生产区块
  • start_block:初始化当前区块数据
  • produce_block:打包、签名、提交区块入链
  • on_incoming_block:接收net_plugin收到的区块,入本节点链
  • on_incoming_transaction_async:接收net_plugin收到的交易,入本节点链
  • on_block:本节点区块入链成功后,相关状态更新
  • on_irreversible_block:本节点区块不可逆后,相关状态更新

下图为producer_plugin生产、同步区块的核心代码逻辑:

4.1 生产区块

如上所示,producer_plugin启动后,调用schedule_production_loop()函数,该函数是插件的最核心的函数,是一个递归函数,负责无限循环出块。
EOS500ms出一个块,因此需要启动一个定时器_timer,逻辑如下:

  • 关闭之前的定时器:_timer.cancel()
  • 调用start_block()函数初始化新的区块信息。首先调用controller::abort_block()重置数据,然后调用controller::start_block(),根据上一个区块信息生成一个新的区块,最后将由于controller::abort_block()而未来得及入链的交易(unapplied_transactions)重新push_transaction()进新的区块。
  • 重启定时器_timer(),异步等待新区块截止时间,这期间内,EOS等待并接收用户的交易请求。
  • 如果用户有新交易,调用push_transaction()执行交易并保存到区块中。
  • 如果没有其它情况,定时器到期后,调用produce_block(),对该区块进行打包(finalize_block())、签名(sign_block()),然后提交(commit_block())到本节点区块链(fork_db)上,commit_block()会发送accepted_block信号给订阅者,这其中包括producer_plugin::on_block(),该函数进行相关数据更新。fork_db每次新增一个区块,就会检查是否有新的不可逆区块产生,如果有,发送irrerersible_block信号给订阅者,这其中包括producer_plugin::on_irreversible(),该函数进行相关数据更新。最后,再次递归调用schedule_production_loop()进行下一个区块生产。
  • 如果出现其它原因,如收到网络上发送过来的区块,且定时器未到期,则会转入区块同步逻辑,之前的所有执行会被重置,未来得及执行的交易会被备份到unapplied_transactions中。

4.2 同步区块

producer_plugin的主流程是schedule_production_loop(),其中定时器_timer会根据实际情况设置等待时间。如是轮到该节点生产区块,则每次等待的时间为:总时间(500ms)-已用时间。如果未轮到该节点生产区块,则计算下一次出块时间(more than 500ms),并启动定时器等待。
如果net_plugin插件或bnet_plugin插件收到网络上的区块,则需要同步。调用on_incoming_block()函数,该函数内部逻辑与生产区块逻辑类似,主要调用controller的abort_block(),start_block(),push_transaction(),finalize_block(),commit_block()。
注意,http和cleos提供了一些接口,其中包括push_transaction,push_block接口,其逻辑见上图,比较特殊,不做太多解释。
下图给出了区块同步的更详细的说明:

重点做以下解释:

  • fork_db:区块链,其中的区块尚未不可逆,当新块到达后,可能存在分叉。需要根据最长链原则,选出较长的一条链,较短的链被删除。
  • 每个新区块基于其上一个区块出块,根据该信息,可以对旧链fork_db的head block(HB)和新的区块(new HB)分别进行追溯,将旧链中的区块合并到新链上,然后删除旧链,保存新链。主要代码在fetch_branch_from()中实现,思想请参考:拉链法

5 net_plugin

net_plugin的主要功能如下:

  • 与其他节点建立连接;
  • 向网络中广播本节点区块;
  • 接收其他节点广播的区块;
  • 节点之间区块同步。

net_plugin的代码结构如下:

5.1 与其他节点建立连接

见上图:

  • net_plugin插件启动时,根据p2p-listen-endpoint成员变量(配置文件或命令行参数p2p-listen-endpoint参数(默认值0.0.0.0:9876)),调用tcp::acceptor的listen()启动监听,调用start_listen_loop()递归调用async_accept()异步等待网络连接请求(参考socket通信)。
  • 根据supplied_peers成员变量(配置文件或命令行参数p2p-peer-address(可多个)),逐一调用async_connect()进行连接请求。
  • 其他节点收到请求后回复,并调用start_session()建立会话。
  • 本节点收到回复得知成功后,亦调用start_session()建立会话。
  • start_session()包含两步:
  • 调用start_read_message()异步循环等待其他节点的消息;
  • 调用send_handshake()发送握手消息。
  • net_plugin插件中定义了9个消息类型,其中主要消息为:
  • handshake_message:握手消息
  • notice_message: 通知消息,主要在同步时使用,进行同步状态发送
  • sync_request_message:当本节点区块链的HB number小于对方节点LIB number时发送
  • request_message:当本节点区块链的HB number小于对方节点HB number时发送
  • signed_block_message:每个区块由该消息逐一发送

5.2 向网络中广播本节点区块

net_plugin插件在启动时,订阅了chain_plugin插件的accepted_block信号,该信号在区块被提交到本地待确认不可逆数据库(fork_db)中后发送。
收到该信号后,net_plugin插件向所有连接中的网络节点广播signed_block_message。

5.3 接收其他节点广播的区块

其他节点的net_plugin插件的start_read_message异步循环等待网络消息,收到其他节点signed_block_message后,会进行判断,如果本节点没有该区块且该区块合法,则保存到本地fork_db中,如果存在分叉,则按照最长链的原则,尝试进行合并,再启用该最长链。

5.4 节点之间区块同步

每个BP连续出块12个(12*0.5s=6s),每出一个块便立即广播,理想中各节点的区块链是实时同步的,然而由于网络原因,或者后加入的节点,其往往落后其他节点很多区块。因此涉及到大量区块的同步问题。
各节点每次建立连接时会发送 handshake_message ,该消息主要用于区块同步。每次握手进行一次区块同步状态判断,同步完成后会再次发送 handshake_message,循环进行判断。
同步状态有5中场景:

  • [State 0]双方HB id相同,id为数字摘要,如果相同说明HB完全一致,不需要同步,则Alice向Bob发送notice_message。
  • [State 1]如果Alice的区块链非常短,其HB number 竟然没有对方节点LIB number大,则Alice向Bob发送sync_request_message,附带参数(start,end)表示同步区间。其中start为Alice的LIB number,end为Bob的LIB number。
  • [State 2]与State 1相反,如果Alice发现Bob的HB number < Alice的LIB number,则发送notice_message让Bob主动来请求同步数据。
  • [State 3]如果Alice的HB number >Bob 的LIB number,但Alice的HB number < Bob的HB number,则也需要同步,Alice向Bob发送request_message,Bob收到消息后,从Bob的LIB开始,一直到Bob的HB,逐一发送区块。
  • [State 4]与State 3相反,则Alice发送notice_message让Bob主动来请求同步数据。
5.4.1 同步状态0

同步状态1见总图,由于较简单,不再赘述。

5.4.2 同步状态1


如上图所示:

  • Alice向Bob请求区块同步,总区间为[Alice LIB + 1, Bob LIB]。
  • 调用request_next_chunk()对总区间分组同步,默认大小为100个区块一组(配置文件或命令行参数sync-fetch-span)。
  • net_plugin::sync_manager子模块顺序选择一个网络节点,发送消息,并启动定时器_timer异步等待5秒。如果5秒后未收到对方的signed_block_message,则取消该请求(再次发送sync_request_message,但区间为[0,0]),并重新选择一个网络节点发送消息。
  • 如果5秒内收到对方节点的signed_block_message,取消_timer定时器,判断是否是自己想要的区块,保存后判断是否同步结束。
  • 如果尚未同步结束,重启_timer定时器等待;
  • 如果分组同步结束,再次调用request_next_chunk()请求下一个分组区块;
  • 如果总区间同步结束,向所有网络节点再次发送握手消息,继续进行同步状态判断。
  • 对方节点收到sync_request_message后,按请求区块区间,循环逐一发送区块。
5.4.3 同步状态2


如上图所示,Bob收到Alice的通知后,与Alice的同步状态1类型,调用相关函数进行同步。

5.4.4 同步状态3

如上图所示,Alice向Bob请求区块,Bob收到消息后,以区间[Bob LIB+1,BOB HB]循环逐一发送区块。
如果Bob的HB number 为0,则为异常,需要告知Alice。

5.4.5 同步状态4

如上图所示,该状态下,参考同步状态3。
另附消息发送函数enqueue()逻辑:

5.5 补充

net_plugin插件启动并建立连接后,会调用start_monitors(),一是监听网络连接状态,如果断开会重新尝试建立连接;二是监听交易时间,如果交易超时,则会将其移除入链队列。

6 controller

Controller模块位于/libraries/chain/下,是EOS区块入链的核心控制器,内容非常多,也非常重要。
Controller主要功能为:

  • 被chain_plugin初始化和启动
  • 对上提供区块和交易等相关接口
  • 对上提供区块和交易入链进度相关信号
  • 对下操作相关数据接口,进行数据管理

下图为Controller核心代码逻辑:

6.1 被chain_plugin初始化和启动

Controller模块由chain_plugin负责初始化和启动,chain_plugin根据启动参数启动Controller,从而对底层数据结构进行初始化操作。

6.2 对上提供区块和交易等相关接口

区块相关的接口有:

  • abort_block() :取消上一个正在生产的区块,其中的交易转到本次新区块中处理
  • start_block() :开始一个新区块生产,启动一个异步定时器等待交易被插入,定时器结束后开始打包等后续工作。此处还会进行BFT共识机制的处理。
  • finalize_block():本区块时间已到,打包区块
  • sign_block():对区块进行签名
  • commit_block():提交区块入fork_db,等待不可逆
  • push_block() : 主要用于初始化时或同步时插入区块,内部调用apply_block()函数以及上述几个函数,但没有异步定时器等代码。
  • pop_block():同步时fork_db发现分叉,需要对短链进行pop_block()
  • on_irreversible():区块不可逆时触发

交易相关的接口有:

  • push_transaction() : 插入交易
  • push_scheduled_transaction() : 插入延期的交易

其它接口:

  • set_proposed_producers() :更新BP列表

6.3 对上提供区块和交易入链进度相关信号

提供了一些进度信号,producer_plugin、mongodb_plugin、net_plugin等等会进行订阅,从而完成各自的功能。
相关信号有:

  • pre_accepted_block :调用push_block()(同步、刚启动时从数据库恢复),区块尚未add()到fork_db之前,先发送这个信号
  • accepted_block_header:调用push_block()或commit_block()(生产区块),区块被add()到fork_db后,发送该信号
  • accepted_block:调用commit_block(),区块被add()到fork_db后,发送该信号
  • irreversible_block:调用push_block()恢复数据时或on_irreversible()时,发送该信号
  • accepted_transaction:调用push_transaction()或push_scheduled_transaction()成功后,发送该信号
  • applied_transaction,同上

例如,mongodb_plugin收到accepted_block后会将数据写入mongodb。

6.4 对下操作相关数据接口,进行数据管理

数据结构包括如下:

  • pending :正在生产的区块,abort_block(),start_block()等均对它修改
  • unapplied_transactions:上一个区块未出块成功,则其交易在abort_block()时转存到这里,以便新的区块start_block()时重新push_transaction()
  • fork_db:区块生产完成后插入这里,可能存在分叉。当进行区块同步时,如果检测到分叉,则进行最长链的生成和选择。push_block()和commit_block()时调用add(),不可逆时或分叉时调用erase()
  • head : 指向fork_db的头块,用于生成下一个区块,以及进行快速比较等操作,fork_db变化则同时变化head
  • reversible_blocks:可逆的区块链。commit_block()时调用add(),on_ireeversible(),pop_block()时调用erase()
  • blog:不可逆的区块链,append only,on_irreversile()时调用append()
  • db : 不仅保存了区块,还保存了智能合约数据,账号数据等等其它数据。大部分数据修改的地方也会对其进行更新。

注意:

  • 区块广播时发送的是signed_block结构体,其继承signed_block_header-->block_header。
  • 各节点存储的数据还有更多信息,如DPOS+BFT相关的数据,存储在block_state-->block_header_state中。
  • signed_block中存储了所有交易的摘要:vector

7 eosio.system

主要实现了抵押RAM、申请BP、申请代理,投票、选举BP等功能。

转载自:https://my.oschina.net/u/4069047/blog/3005068

关于EOS出块BP备份的想法

假设使用一台机器作为BP节点机器。大家都知道eos的data目录超级大,备份的话只能全量备份。cp 的时间过于长,影响出块和RPC业务等提供。

暂时想到的方案如下

机器上跑2个nodeos进程,一个跑BP账号做出块节点,一个做同步几点。
保留三份data数据。出块节点初始化占一个,剩余两个,其中一个同步节点同步使用,同步节点定期切换剩余两个目录,保持2个目录数据与主网持平,当出块节点出问题导致data脏时,时切换到当时没有占用的data目录。然后删除脏目录,停止同步节点,并cp 同步节点前面用的data目录补齐第三目录。然后选一data重新运行同步节点。

且如果此机器提供RPC服务的话,可以做高可用。

优点

本方案可保证出块节点最大的运行时间。

缺点

data 三份的占用。

关于资源的抵押赎回,及查询抵押列表

关于抵押和赎回的方法,拿eosjs举例
https://eosio.github.io/eosjs/guides/2.-Transaction-Examples.html

Stake 抵押

const result = await api.transact({
  actions: [{
    account: 'eosio',
    name: 'delegatebw',
    authorization: [{
      actor: 'useraaaaaaaa',
      permission: 'active',
    }],
    data: {
      from: 'useraaaaaaaa',
      receiver: 'useraaaaaaaa',
      stake_net_quantity: '1.0000 SYS',
      stake_cpu_quantity: '1.0000 SYS',
      transfer: false,
    }
  }]
}, {
  blocksBehind: 3,
  expireSeconds: 30,
});

Unstake 赎回

const result = await api.transact({
  actions: [{
    account: 'eosio',
    name: 'undelegatebw',
    authorization: [{
      actor: 'useraaaaaaaa',
      permission: 'active',
    }],
    data: {
      from: 'useraaaaaaaa',
      receiver: 'useraaaaaaaa',
      unstake_net_quantity: '1.0000 SYS',
      unstake_cpu_quantity: '1.0000 SYS',
      transfer: false,
    }
  }]
}, {
  blocksBehind: 3,
  expireSeconds: 30,
});

操作细节看以上demo即可,下面我们说下抵押时transfer这个参数。
先看合约 (跳转代码)

if ( transfer ) {
    from = receiver;
}

如果设置transfer参数为true,及把来源的账户修改为接收账户。
也就变成了接收账户自己给自己抵押,由来源账户付钱

查看数据

查看代码
抵押数据表明为delband
(查看代码)

del_bandwidth_table     del_tbl( _self, from.value );

是以来源账户为查询的scope
所以RPC查询如下
post: https://api.eoslaomao.com/v1/chain/get_table_rows
data:

{
    "scope": "bcskillsurou",
    "code": "eosio",
    "table": "delband",
    "json": true
}

返回

{
    "rows": [
        {
            "from": "bcskillsurou",
            "to": "bcskillsurou",
            "net_weight": "0.0046 EOS",
            "cpu_weight": "0.0147 EOS"
        }
    ],
    "more": false
}

EOS 修改链代码 默认公钥前缀

https://github.com/EOSIO/fc/blob/df5a17ef0704d7dd96c444bfd9a70506bcfbc057/include/fc/crypto/public_key.hpp#L11

namespace fc { namespace crypto {
   namespace config {
      constexpr const char* public_key_legacy_prefix = "EOS"; // 修改EOS为需要的前缀即可,如BSC

EOS BCSkill技术论坛 区块链求职招聘

BCSkill技术社区论坛开放

为了方便问题收集,促进学习激情,我们的BCSkill技术社区新建一个论坛。
网址:http://wiki.bcskill.com/

欢迎学习EOS等区块链技术的小伙伴,以及求职招聘(招聘加群请注明)相关的加入~

求职板块:http://wiki.bcskill.com/?forum-2.htm
招聘版块:http://wiki.bcskill.com/?forum-3.htm
我们社区内有很多区块链技术不错的小伙伴,有需要人手的,快来发帖吧~

此贴定期更新一批邀请码,或者加QQ群791420381直接获取,邀请码永远是免费的。只是为了防止垃圾广告帖,减少不必要的内容维护。

邀请码 (20190626更新)

8D93BADE852A
0B9E8C1154C9
BDBDE23003F0
022DD038C8B6
4F769BD9E717
5E99E0623F53
AECC1DFBF089
942FAA28A72E
029DB38EF967
6E2D228B1A7A
4E531F0F603C
74203AB799D8
29E5197CDD37
A4205E8F887C
CA14F3FC6CD5
8EDE670F3158
3C1CBD30D732
C1223552764A
9E55D43A0B31
94CAF29753D3
0D2DD72A3B8D
347D14F7B608
70EDBDE5BF14
9E5D6377CDE6
EFB9A497172F
0C27D7E52EA7
FBFE103795C8
E8C077491575
4935C0BCD9CA
A3434A1FC97D
D2D9FCF33093
FA52947D11B0
5486B5339604
FDA85E994D7C
A40F20A9982D
9CD1F59EFA91
B6703CDFB303
FA2FC5376CAC
DCEE07FBDF26
F485EEAE2293
41BEC6014BB9
4F65898AF09F
9345EC014A71
F630E100BC1D
F949ED29A2E0
D3ACFD1FC374
234A91A79039
65C94C6763EA
1C0A73B415BF
86A44E0B5AF0