ACS:AElf Contract Standard,AElf 合约标准,顾名思义,就是开发 AElf 智能合约时需要继承和实现的一些接口。
所有的 ACS 都通过 protobuf 的 service 定义。ACS4 作为 ACS 之一,是实现任意一种共识合约时,需要实现的一些接口。
本文主要讨论共识标准接口的可行性和 AElf 中为共识合约的实现所提供的接口及其他支持。
抽象共识的思路
站在实现区块链共识的角度,我们主要关心三件事:
谁可以产生区块,如 PoW 共识允许每一个人参与算力竞争,PoS 和 DPoS 共识则要对此做出一定限制;
如果可以产生区块,那么应该在什么时候产生,或者说当前的时间能不能开始尝试广播区块;
作为一个区块链全节点,应该怎么验证这个区块的合法性。
针对这三点,我们很容易想到三类接口:
输入公钥,判断这个公钥有没有资格产生区块,PoW 共识直接返回 true 就可以了,DPoS 可能会对大部分人返回 false。
输入公钥,返回这个公钥下一次能够产生区块的时间戳,或者返回这个公钥当前能不能产生区块。
全节点得到区块头(block header)之后,输入从中提取到的共识数据,验证这个区块的 1 和 2 相关信息,即,PoW 共识需要验证 nonce 的合法性,DPoS 共识需要验证新区块的生产者身份合法性、生产者是否尊重自己的时间槽,得到验证结果。
除此以外,为了得到接口 3 中的输入,即区块头中的共识数据,至少还需要为区块生产者提供一个生成区块头共识数据的方法。
AElf 中的实践
首先,AElf 的主链选择的共识属于 DPoS,本文虽说讨论的是通用共识接口,也免不了倾向于多讨论(AE)DPoS。
其次,所有的共识合约标准上的接口,都是只读的,因为单纯获取这些数据无需改动 WorldState。(WorldState 是以太坊中的概念,AElf 在开发中称用于存储合约的状态的数据库为 State DB;除此之外还有 Chain DB,用于存储区块本身,包括区块中的交易。)
ACS4 中合并了接口 1 和接口 2,得到一个接口:
rpc GetConsensusCommand (google.protobuf.BytesValue) returns (ConsensusCommand) {
option (aelf.is_view) = true;
}
message ConsensusCommand {
int32 NextBlockMiningLeftMilliseconds = 1;// How many milliseconds left to trigger the mining of next block.
int32 LimitMillisecondsOfMiningBlock = 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 ExpectedMiningTime = 4;
}
很显然,NextBlockMiningLeftMilliseconds 的值取决于 ExpectedMiningTime(预期出块时间)与当前时间(调用这个接口的时间)的差值。
LimitMillisecondsOfMiningBlock 是系统分配给这个块用于打包的时间,决定了这个区块能够打包多少用户发送的交易(给打包用户发送的交易留了多长时间)。
Hint 字段用来传递一些单独的状态,取决于选用的共识协议,在 PoW 中这个字段可能显得不必要,而 AEDPoS 定义了一些区块类型,如 Normal Block,Extra Block,只更新少量共识信息的 Tiny Block。这些,都需要反应在 Hint 中——共识合约不仅需要告知区块生产者多久以后可以着手产生区块,也需要告知他应该产生什么类型的区块,而不同区块会更新不同的共识信息。除此之外,引入 Hint 字段也能为实现其他共识协议提供了更多的可能。
这个接口分别会在链刚刚启动时、在本地 Best 分支上同步完一个新的区块(无论这个区块是不是自己产生的)时、区块生产者在执行自己的区块前的验证中忽然发现当前时间已经超出了自己的时间槽(即调用下文的 ValidateConsensusBeforeExecution 接口)后被调用。
调用之后,Consensus Service 会将 NextBlockMiningLeftMilliseconds 传入共识的调度器中,时间一到,就会去触发生产区块的逻辑。
注意,调度器中的时间是可以随时被覆盖的。事实上,每一次同步一个新的区块,调度器都会被重新初始化。
事关接口 3,ACS4 拆分出两个接口:
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;
}
也就是说,AElf 中,每一个区块执行前后,会分别调用以上两个接口的实现对区块头进行验证。验证的逻辑位于实现 ACS4 的合约中,验证的数据就不得不基于 State DB。因为你不能凭空去验证区块头里的共识信息对不对,共识合约里需要存储最近的共识信息,才能给最近的共识信息提供验证的支撑。
既然验证的数据基于 State DB,而 ACS4 的接口都是只读的方法,那就需要单独生成一笔交易去更新 State DB(区块链的特性,更新 WorldState 或者 State DB 必须通过执行交易来完成)。
这样,ACS4 除了要提供生成区块头信息的方法之外,还需要能够返回一个(或者一组)能够更新 State DB 中的共识信息的交易。这个交易会作为系统交易被生成,然后在打包过程中,最先添加进区块中。系统交易的执行会先于普通交易,这样普通交易在执行时会获取到最近更新的系统提供的信息,如 hash-commit-reveal 策略下的随机数。在 AElf 中,每个区块会至少包含一个共识系统交易。
ACS4 的最后两个与更新共识信息相关的接口如下:
rpc GetInformationToUpdateConsensus (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 TransactionList {
repeated aelf.Transaction Transactions = 1;
}
在 AElf 的 kernel 模块代码中,GetInformationToUpdateConsensus 会在生成区块头的过程中被调用,该接口返回的数据会作为区块头的 Extra Data 之一,用于收到区块的人验证共识信息。GenerateConsensusTransactions 接口会在生成区块头之后,进一步生成系统交易的过程中被调用。补充一句,ValidateConsensusAfterExecution 接口本质上只是为了验证区块头中的共识信息和执行完共识系统交易后 State DB 中的共识信息的一致性,防止区块生产者“出尔反尔”。
ACS4 的完整代码在这里。关于对本文或者 ACS4 或者其他 AElf 共识组件的建议都会被认真对待,欢迎留言、私信甚至直接去 github 开 issue。
以上是 AElf 区块链共识合约标准 ACS4 的大致介绍,下一篇文章会介绍其中最为复杂的 GetConsensusCommand 接口在 AEDPoS 共识下的具体实现。