技术点解读 | aelf共识标准设计

区块链系统共识:去中心化的共识
本质上,区块链系统是一个分布式系统,但是与普遍的分布式系统不同。普遍的分布式系统,其意义在于:面对增长的业务量,用多台机器承载垂直拆分或水平拆分后的业务场景,增大系统容量;根据业务的关键程度,消除单点故障,加强系统可用性。当一个区块链系统承担的业务场景复杂如普遍的分布式系统时,当然也需要做如上的考虑。但是区块链系统之所以应当被人重视,是因为它能够解决存在作恶节点情况下的数据一致性的问题,也就是拜占庭将军问题。
区块链世界中,不存在所谓的中心化服务器,其是由所有爱好者、受益者或其他相关人共同构成的P2P网络,网络中的任何一个节点都是不可直接信任的,它们中的任何一个都有作恶可能,这是普遍的分布式系统并不会考虑的问题。这一点,与拜占庭将军问题的假设一致:没有中心化的领导机构,这些将军需要对某个城市发起攻击时,所有将军需要对任何将军提出的攻击时间达成共识。那么问题来了,如果将军们自己决定的攻击时间不一致,甚至于有将军已经成为叛徒,那么将军们如何达成共识呢?
同理,在区块链系统这个P2P网络中,所有的节点如何针对某一笔交易达成共识呢(也就是基于这一笔交易对节点各自的数据库做出修改)?
在1982年的论文The Byzantine Generals Problem中,Leslie Lamport证明,当将军们中的叛徒不超过1/3时,存在有效的算法,无论叛徒们如何折腾,忠诚的将军们总能达成一致的结果。而如果叛徒过多,就无法保证一定能达到一致。
那么我们直接假设区块链P2P网络中,作恶节点数量不超过1/3,否则认为区块链系统构建失败。如此,接下来最难解决的问题就是,在一个作恶节点不超过1/3的区块链系统中,要选择谁的数据作为达成最终共识的数据?
换一个角度:如果一个节点希望自己提供的数据能够在区块链系统中达成共识,他需要做什么?他需要提供一个Proof。一个证明。去说服区块链系统接受他所提供的数据。
据此,我们开始讨论如何在区块链系统中设计一个标准共识接口。
标准共识接口设计
区块链达成共识的流程:
1.节点A准备了一个区块,广播给P2P网络;
2.P2P网络的其他节点收到区块以后,经过一系列验证,决定是否将该区块放在本地的最长链上;

3.当区块链系统中的大多数节点(如超过2/3)本地某个区块高度对应的区块哈希值都一致的话,我们就可以认为区块链针对这个高度的区块达成了一致。

如果需要一个服务来帮助节点A及区块链其他节点完成整个共识过程,那么所提供的服务应该大体上有两种:
1.面对一无所知的A,需要在A询问的时候,告知其(在区块链世界中,A使用一个公钥唯一确定身份)当前能不能尝试产生区块,以及如何设法使其产生的区块让其他节点接受;
2.除了A以外的其他节点,面对从网络上收到的一个A广播出来的区块,通过一个开源的所有节点实现代码都一致的服务来验证该区块是否合法。
如果某节点通过对这个区块的验证,得知该区块合法,则称该节点对A产生的这个区块达成了共识。因为所有节点的验证服务都是同样的逻辑,区块链网络中所有节点对该区块的合法性都会具有一样的态度,终究,这一个区块链P2P网络中(在没有更长的链出现的情况下)对这个区块被添加到最长链这个事件达成最终一致性也是可以预见的。
aelf共识通用接口标准
现在开始,我们基于“标准共识接口设计”中统计出来的两类服务,进行aelf共识通用接口的设计。
首先需要明确,这两类和共识相关的服务(请求区块生产相关的指示、验证新区块)都是只读的接口,其调用本身无需修改区块链网络的账本信息。
其次,这些接口实际上会被aelf主链代码调用,因此其设计需要遵循aelf主链代码中关于生产区块和验证区块的逻辑(当然,即便在主链代码中,这些接口也几乎一一对应地出现在共识的服务Consensus Service中)。
我们分别讨论两种接口:
请求共识命令
继续前面的例子,还是节点A,这是一个已经同步到当前aelf最长链的节点。当前时间是2020年1月1日下午13:59:56。A,作为一个诚实的节点(没有修改本地主链代码),刚刚同步了一个区块(也就是接受到网络上其他节点的区块,验证成功,修改了本地的区块链账本信息),本地的Best Chain(维护本地区块链的一个数据结构)得到更新后,Event Bus上装载了一个事件。这个事件的作用之一,就是提醒节点A去问一下共识服务(通过相关事件订阅和处理机制),接下来他能做点什么。在进行询问时,A把自己的公钥传给了共识服务。
共识服务的核心逻辑作为一个智能合约而存在,因为只有如此才能保证其代码对于区块链世界中每一个节点都是一致的(不一致意味着这个节点试图作恶或者硬分叉)。经过长达几毫秒的复杂计算(也许是简单计算),共识的智能合约反馈给节点A一个信息。这个信息的生成就因共识机制的选取而异,但是无论什么共识,都应该具备以下结构:
· A什么时间可以产生区块?
· 如果A可以产生区块,那么A应该用什么方式进行下一步的请求:即在当前共识下,A能产生什么区块。在此称这一信息为额外提示。
如果A不能产生区块怎么办?区块链世界中理论上每个人其实都有可能产生区块,但是由于共识机制的设计不同(比如PoS共识),有些区块链并不希望大多数节点有生产区块的权利。这种情况下,只需要将返回给A的时间设置到一百年后就可以(可能有些夸张,但是几个月后总没问题)。只要节点A能够坚持挂机,并且区块链没有产生任何一个新的区块(任何有效的新的区块的同步都会使节点A重新获得一个出块时间)。
不难想象基于这个接口实现PoW有多么容易。只要时间设置为“立刻”,额外提示为空即可。
在aelf主链中,共识服务得知共识反馈的时间信息后,会立刻更新共识调度器(如果此前共识调度器非空,则干掉之前未竟的调度信息,用新的时间点来填充,也就是说共识调度器里面只能有一个未执行的共识任务,且共识调度器是单例的对象)。
接下来就是漫长的倒计时。
我们回到节点A这个例子。假设A在请求共识命令后,得到了一个时间:2020年1月1日下午14:00:00,也就是4秒钟以后。额外提示:NextRound(这是AEDPoS共识的一个提示,意味着A将终结本轮的出块流程,并更新下一轮的所有代理出块节点的出块顺序)。这就意味着调度器会立刻更新为4秒后执行一个生产区块的事件。这4秒中做什么?如果可以同步到其他节点发过来的区块,而这些区块可以通过验证,那就使用Best Chain更新这一事件的处理器,不断地问共识服务请求共识命令(这一个操作在代码中称为TriggerConsensus),相应的,共识调度器就会不断地重置:3.5秒,3秒,2.5秒,2秒,……
终于,时间来到了14:00:00。节点A在共识调度器的支配下开始准备生产区块。此时,按照我们之前的设计,除了已经发挥了作用的出块时间,关于如何生产区块,它唯一知道的信息只有之前共识服务给他的额外提示。
这时,在aelf中,节点A把额外提示信息传递给共识服务。在打包交易之余,还会调用另外两个服务:
· 获得共识区块头信息
· 获得共识系统交易
请求共识命令的接口有一个作用是设法让生产出来的区块通过验证。在aelf中,在区块的一系列验证步骤中,有两个和共识相关的验证:执行前,验证区块头;执行后,对共识合约状态的修改信息是否和区块头中的信息的一致性进行验证。
简单做个类比,一个.NET程序员去参加DNT线下沙龙,他拿出参加沙龙的邀请短信给沙龙主办方进行查验,这个短信类似于区块头,也就是说如果他拿不出邀请短信,那主办方不会让他参加。接下来,主办方还会要求.NET程序员报出手机号,然后在参会人员的花名册中寻找该手机号,这就类似于在区块链节点执行完共识交易后的验证。只有这一步也验证通过,.NET程序员才能顺利参加此次沙龙。
综上所述,针对“请求共识命令”这一类服务,我们需要三个接口。用Protobuf直接描述如下:
service ConsensusContract {
    rpc GetConsensusCommand (google.protobuf.BytesValue)
returns (ConsensusCommand) {
        option (aelf.is_view) = true;
    }
    rpc GetConsensusExtraData (google.protobuf.BytesValue) 
returns (google.protobuf.BytesValue) {
        option (aelf.is_view) = true;
    }
    rpc GenerateConsensusTransactions (google.protobuf.BytesValue) 
returns (TransactionList) {
        option (aelf.is_view) = true;
    }
}
message ConsensusCommand {
    int32 limit_milliseconds_of_mining_block = 2;
    // Time limit of mining next block.
    bytes hint = 3;
    // Context of Hint is diverse according to the consensus protocol we choose, so we use bytes.
    google.protobuf.Timestamp arranged_mining_time = 4;
    google.protobuf.Timestamp mining_due_time = 5;
}
message TransactionList {
    repeated aelf.Transaction transactions = 1;
}
出于对链的安全和稳定性考虑,在ConsensusCommand中,除了下次出块时间(arranged_mining_time)和额外提示(hint),还包括了出块时间限制(limit_milliseconds_of_mining_block)和最晚广播时间(mining_due_time)。后面两个信息都是给区块生产服务作为参考的,用来实现如果超过了某个时间限制,生产出来的区块就无需广播(或者即便广播别的节点也不能通过验证,当然这个验证是在下面要讨论的接口类型的具体实现中保证的);多生产出一个块也比扰乱区块生产秩序要好。
区块验证
如果说请求共识命令还值得细致讨论的话,区块验证相关的接口就泛善可陈了。因为区块验证逻辑本质上是完全因共识而异的。
接口本身并无新意,一个是在共识交易执行前验证区块头,一个是共识交易执行后验证共识修改的状态是否和区块头中承诺的信息一致。而两个验证接口的入参都是二进制数组,意味着该接口接受任何数据,只需要共识的实现者在验证的具体实现中自行反序列化即可。
service ConsensusContract {
    rpc ValidateConsensusBeforeExecution (google.protobuf.BytesValue) 
returns (ValidationResult) {
        option (aelf.is_view) = true;
    }
    rpc ValidateConsensusAfterExecution (google.protobuf.BytesValue) 
returns (ValidationResult) {
        option (aelf.is_view) = true;
    }
}
message ValidationResult {
    bool success = 1;
    string message = 2;
    bool is_re_trigger = 3;