AEDPoS合约实现之GetConsensusCommand

正如文章AElf共识合约标准中所述,GetConsensusCommand接口用于获取某个公钥下一次生产区块的时间等信息。

在AEDPoS的实现中,其输入仅为一个公钥(public key),该接口实现方法的调用时间另外作为参考(其实也是一个重要的输入)。AElf区块链中,当系统内部调用只读交易时,合约执行的上下文是自行构造出来的,调用时间也就是通过C#自带函数库的DateTime.UtcNow生成了一个时间,然后把这个时间转化为protobuf提供的时间戳数据类型Timestamp,传入合约执行的上下文中。

事实上,无论要执行的交易是否为只读交易,合约代码中都可以通过Context.CurrentBlockTime来获取当前合约执行上下文传进来的时间戳。

本文主要解释AEDPoS共识如何实现GetConsensusCommand。在此之前,对不了解AElf共识的聚聚简单介绍一下AEDPoS的流程。

AEDPoS Process

DPoS的基本概念我们不再赘述,假设现在AElf主链通过投票选举出17个节点,我们(暂时地)称之为AElf Core Data Center,简称CDC。(对应eos中的BP即Block Producer这个概念。)

这些CDC是通过全民投票在某个区块高度(或者说时间点)的结果,直接取前17名得到。每次重新统计前17名候选人并重新任命CDC,称为换届(Term)。

在每一届中,所有的CDC按轮(Round)次生产区块。每一轮有17+1个时间槽,每位CDC随机地占据前17个时间槽之一,最后一个时间槽由本轮额外区块生产者负责生产区块。额外区块生产者会根据本轮每个CDC公布的随机数初始化下一轮的信息。18个时间槽后,下一轮开始。如此循环。

Round的数据结构如下:

1// The information of a round. 2message Round { 3 sint64 round_number = 1; 4 map<string, MinerInRound> real_time_miners_information = 2; 5 sint64 main_chain_miners_round_number = 3; 6 sint64 blockchain_age = 4; 7 string extra_block_producer_of_previous_round = 7; 8 sint64 term_number = 8; 9} 10 11// The information of a miner in a specific round. 12message MinerInRound { 13 sint32 order = 1; 14 bool is_extra_block_producer = 2; 15 aelf.Hash in_value = 3; 16 aelf.Hash out_value = 4; 17 aelf.Hash signature = 5; 18 google.protobuf.Timestamp expected_mining_time = 6; 19 sint64 produced_blocks = 7; 20 sint64 missed_time_slots = 8; 21 string public_key = 9; 22 aelf.Hash previous_in_value = 12; 23 sint32 supposed_order_of_next_round = 13; 24 sint32 final_order_of_next_round = 14; 25 repeated google.protobuf.Timestamp actual_mining_times = 15;// Miners must fill actual mining time when they do the mining. 26 map<string, bytes> encrypted_in_values = 16; 27 map<string, bytes> decrypted_previous_inValues = 17; 28 sint32 produced_tiny_blocks = 18; 29}

在AEDPoS合约中有一个map结构,key是long类型的RoundNumber,从1自增,value就是上述的Round结构,CDC产生的每个区块都会更新当前轮或者下一轮的信息,以此推进共识和区块生产,并为共识验证提供基本依据。

AEDPoS的流程大致如此。如果对其中技术细节感兴趣,可见AElf白皮书中的4.2.4节。对实现细节感兴趣,可见Github上AEDPoS共识合约项目

ConsensusCommand

AElf共识合约标准中提到过ConsensusCommand的结构:

1message ConsensusCommand { 2 int32 NextBlockMiningLeftMilliseconds = 1;// How many milliseconds left to trigger the mining of next block. 3 int32 LimitMillisecondsOfMiningBlock = 2;// Time limit of mining next block. 4 bytes Hint = 3;// Context of Hint is diverse according to the consensus protocol we choose, so we use bytes. 5 google.protobuf.Timestamp ExpectedMiningTime = 4; 6}

对于AEDPoS共识,Hint为CDC下一次生产什么类型的区块指了一条明路。我们为Hint提供了专门的数据结构AElfConsensusHint:

1message AElfConsensusHint { 2 AElfConsensusBehaviour behaviour = 1; 3}

而区块类型正包含在如下的Behaviour中:

1enum AElfConsensusBehaviour { 2 UpdateValue = 0; 3 NextRound = 1; 4 NextTerm = 2; 5 UpdateValueWithoutPreviousInValue = 3; 6 Nothing = 4; 7 TinyBlock = 5; 8}

逐一解释:

UpdateValue和UpdateValueWithoutPreviousInValue代表该CDC要生产某一轮中的一个普普通通的区块。CDC重点要更新的共识信息包括他前一轮的in_value(previous_in_value),本轮产生的out_value,以及本轮用来产生out_value的in_value的密码片段。(该CDC会用in_value和其他CDC的公钥加密得到16个密码片段,其他CDC只能各自用自己的私钥解密,当解密的片段达到一定数量后,原始的in_value就会被揭露;这是shamir's secret sharing的一个应用,细节可谷歌,AElf主链用了ECDH实现,如果以后有机会可以写文章讨论一下。)除此之外,还要在actual_mining_times中增加一条本次实际触发区块生产行为的时间戳。UpdateValueWithoutPreviousInValue和UpdateValue区别仅在于本次不需要公布上一轮的in_value(previous_in_value),因为当前轮是第一轮,或者刚刚换过届(而该CDC是一个萌新CDC)。

NextRound代表该CDC是本轮的额外区块生产者(或者补救者——当指定的额外区块生产者缺席时),要初始化下一轮信息。下一轮信息包括每个CDC的时间槽排列及根据规则指定的下一轮的额外区块生产者。

NextTerm类似于NextRound,只不过会重新统计选举的前17名,根据新一届的CDC初始化下一轮信息。

Nothing是发现输入的公钥并不是一个CDC。

TinyBlock代表该CDC刚刚已经更新过共识信息,但是他的时间槽还没有过去,他还有时间去出几个额外的块。目前每个时间槽最多可以出8个小块。这样的好处是提高区块验证的效率(eos也是这么做的)。

有一个时间槽的问题需要特别注意,由于AEDPoS选择在创世区块中生成第一轮共识信息(即所有最初的CDC的时间槽等信息),而创世区块对于每一个节点都应该是完全一致的,因此第一轮的共识信息不得不指定给一个统一的时间(否则创世区块的哈希值会不一致):如今这个时间是0001年0点。这样会导致第一轮的时间槽极其不准确(所有的CDC直接错过自己的时间槽两千多年),因此在获取第一轮的ConsensusCommand时会做特殊处理。

GetConsensusBehaviour

AEDPoS合约中,要让GetConsensusCommand方法返回ConsensusCommand,首先会根据输入的公钥和调用时间得到AElfConsensusBehaviour。然后再使用AElfConsensusBehaviour判断下一次出块时间等信息。

这里的逻辑相对比较清晰,也许可以用一张图来解释清楚:

相关完整代码见:

AElfProject/AElf

接下来我们逐个讨论每个Behaviour对应的ConsensusCommand。

GetConsensusCommand - UpdateValueWithoutPreviousInValue

AElfConsensusBehaviour.UpdateValueWithoutPreviousInValue的主要作用是实现Commitment Scheme(WiKi词条),仅包含一次_commit phase_,不包含_reveal phase_。对应共识Mining Process的阶段,就是每一届(当然包括第一届,也就是链刚刚启动的时候)的第一轮,CDC要试图产生本轮第一个区块。

如果当前处于第一届的第一轮,则需要从AEDPoS共识的Round.real_time_miners_information信息中读取提供公钥的CDC在本轮中的次序order,预期出块时间即order * mining_interval毫秒之后。mining_interval默认为4000ms。

否则,直接从Round信息中读取expected_mining_time,依据此来返回ConsensusCommand。

1 if (currentRound.RoundNumber == 1) 2 { 3 // To avoid initial miners fork so fast at the very beginning of current chain. 4 nextBlockMiningLeftMilliseconds = 5 currentRound.GetMiningOrder(publicKey).Mul(currentRound.GetMiningInterval()); 6 expectedMiningTime = Context.CurrentBlockTime.AddMilliseconds(nextBlockMiningLeftMilliseconds); 7 } 8 else 9 { 10 // As normal as case AElfConsensusBehaviour.UpdateValue. 11 expectedMiningTime = currentRound.GetExpectedMiningTime(publicKey); 12 nextBlockMiningLeftMilliseconds = (int) (expectedMiningTime - Context.CurrentBlockTime).Milliseconds(); 13 }

GetConsensusCommand - UpdateValue

AElfConsensusBehaviour.UpdateValue包含一次Commitment Scheme中的_reveal phase_,一次新的_commit phase_。对应共识Mining Process的阶段为每一届的第二轮及以后,CDC试图产生本轮的第一个区块。

直接读取当前轮的Round信息中该CDC的公钥对应的expected_mining_time字段即可。

1 expectedMiningTime = currentRound.GetExpectedMiningTime(publicKey); 2 nextBlockMiningLeftMilliseconds = (int) (expectedMiningTime - currentBlockTime).Milliseconds();

GetConsensusCommand - NextRound

AElfConsensusBehaviour.NextRound会根据本轮各个CDC公布的信息,按照顺序计算规则,生成下一轮各个CDC的顺序和对应时间槽,将RoundNumber往后推进一个数字。

对于本轮指定为额外区块生产者的CDC,直接读取本轮的额外区块生成时间槽即可。

否则,为了防止指定的额外区块生产者掉线或者在另外一个分叉上出块(在网络不稳定的情况下会出现分叉),其他所有的CDC也会得到一个互不相同额外区块生产的时间槽,这些CDC在同步到任何一个CDC生产的额外区块后,会立刻重置自己的调度器,所以不必担心产生冲突。

对于第一届第一轮的特殊处理同AElfConsensusBehaviour.UpdateValueWithoutPreviousInValue。

1 ... 2 var minerInRound = currentRound.RealTimeMinersInformation[publicKey]; 3 if (currentRound.RoundNumber == 1) 4 { 5 nextBlockMiningLeftMilliseconds = minerInRound.Order.Add(currentRound.RealTimeMinersInformation.Count).Sub(1) 6 .Mul(currentRound.GetMiningInterval()); 7 expectedMiningTime = Context.CurrentBlockTime.AddMilliseconds(nextBlockMiningLeftMilliseconds); 8 } 9 else 10 { 11 expectedMiningTime = 12 currentRound.ArrangeAbnormalMiningTime(minerInRound.PublicKey, Context.CurrentBlockTime); 13 nextBlockMiningLeftMilliseconds = (int) (expectedMiningTime - Context.CurrentBlockTime).Milliseconds(); 14 } 15 ... 16 17 /// <summary> 18 /// If one node produced block this round or missed his time slot, 19 /// whatever how long he missed, we can give him a consensus command with new time slot 20 /// to produce a block (for terminating current round and start new round). 21 /// The schedule generated by this command will be cancelled 22 /// if this node executed blocks from other nodes. 23 /// </summary> 24 /// <returns></returns> 25 public Timestamp ArrangeAbnormalMiningTime(string publicKey, Timestamp currentBlockTime) 26 { 27 if (!RealTimeMinersInformation.ContainsKey(publicKey)) 28 { 29 return new Timestamp {Seconds = long.MaxValue}; 30 } 31 32 miningInterval = GetMiningInterval(); 33 34 if (miningInterval <= 0) 35 { 36 // Due to incorrect round information. 37 return new Timestamp {Seconds = long.MaxValue}; 38 } 39 40 var minerInRound = RealTimeMinersInformation[publicKey]; 41 42 if (GetExtraBlockProducerInformation().PublicKey == publicKey) 43 { 44 var distance = (GetExtraBlockMiningTime().AddMilliseconds(miningInterval) - currentBlockTime).Milliseconds(); 45 if (distance > 0) 46 { 47 return GetExtraBlockMiningTime(); 48 } 49 } 50 51 var distanceToRoundStartTime = (currentBlockTime - GetStartTime()).Milliseconds(); 52 var missedRoundsCount = distanceToRoundStartTime.Div(TotalMilliseconds(miningInterval)); 53 var expectedEndTime = GetExpectedEndTime(missedRoundsCount, miningInterval); 54 return expectedEndTime.AddMilliseconds(minerInRound.Order.Mul(miningInterval)); 55 }

GetConsensusCommand - NextTerm

AElfConsensusBehaviour.NextTerm会根据当前的选举结果重新选定17位CDC,生成新一届第一轮的信息。方法同AElfConsensusBehaviour.NextRound不对第一届第一轮做特殊处理的情况。

1expectedMiningTime = 2 currentRound.ArrangeAbnormalMiningTime(minerInRound.PublicKey, Context.CurrentBlockTime); 3 nextBlockMiningLeftMilliseconds = (int) (expectedMiningTime - Context.CurrentBlockTime).Milliseconds();

GetConsensusCommand - TinyBlock

AElfConsensusBehaviour.TinyBlock发生在两种情况下:

  1. 当前CDC为上一轮的额外区块生产者,在生产完包含NextRound交易的区块以后,需要在同一个时间槽里继续生产最多7个区块;
  2. 当前CDC刚刚生产过包含UpdateValue交易的区块,需要在同一个时间槽继续生产最多7个区块。

基本判断逻辑是,如果当前CDC为本轮出过包含UpdateValue交易的块,即情况2,就结合当前CDC是上一轮额外区块生产者的情况,把一个长度为4000ms的时间槽切分成8个500ms的小块时间槽,进行分配;否则为上述的情况1,直接根据已经出过的小块的数量分配一个合理的小块时间槽。

1/// <summary> 2 /// We have 2 cases of producing tiny blocks: 3 /// 1. After generating information of next round (producing extra block) 4 /// 2. After publishing out value (producing normal block) 5 /// </summary> 6 /// <param name="currentRound"></param> 7 /// <param name="publicKey"></param> 8 /// <param name="nextBlockMiningLeftMilliseconds"></param> 9 /// <param name="expectedMiningTime"></param> 10 private void GetScheduleForTinyBlock(Round currentRound, string publicKey, 11 out int nextBlockMiningLeftMilliseconds, out Timestamp expectedMiningTime) 12 { 13 var minerInRound = currentRound.RealTimeMinersInformation[publicKey]; 14 var producedTinyBlocks = minerInRound.ProducedTinyBlocks; 15 var currentRoundStartTime = currentRound.GetStartTime(); 16 var producedTinyBlocksForPreviousRound = 17 minerInRound.ActualMiningTimes.Count(t => t < currentRoundStartTime); 18 var miningInterval = currentRound.GetMiningInterval(); 19 var timeForEachBlock = miningInterval.Div(AEDPoSContractConstants.TotalTinySlots);//8 for now 20 expectedMiningTime = currentRound.GetExpectedMiningTime(publicKey); 21 22 if (minerInRound.IsMinedBlockForCurrentRound()) 23 { 24 // After publishing out value (producing normal block) 25 expectedMiningTime = expectedMiningTime.AddMilliseconds( 26 currentRound.ExtraBlockProducerOfPreviousRound != publicKey 27 ? producedTinyBlocks.Mul(timeForEachBlock) 28 // Previous extra block producer can produce double tiny blocks at most. 29 : producedTinyBlocks.Sub(producedTinyBlocksForPreviousRound).Mul(timeForEachBlock)); 30 } 31 else if (TryToGetPreviousRoundInformation(out _)) 32 { 33 // After generating information of next round (producing extra block) 34 expectedMiningTime = currentRound.GetStartTime().AddMilliseconds(-miningInterval) 35 .AddMilliseconds(producedTinyBlocks.Mul(timeForEachBlock)); 36 } 37 38 if (currentRound.RoundNumber == 1 || 39 currentRound.RoundNumber == 2 && !minerInRound.IsMinedBlockForCurrentRound()) 40 { 41 nextBlockMiningLeftMilliseconds = GetNextBlockMiningLeftMillisecondsForFirstRound(minerInRound, miningInterval); 42 } 43 else 44 { 45 TuneExpectedMiningTimeForTinyBlock(miningInterval, 46 currentRound.GetExpectedMiningTime(publicKey), 47 ref expectedMiningTime); 48 49 nextBlockMiningLeftMilliseconds = (int) (expectedMiningTime - Context.CurrentBlockTime).Milliseconds(); 50 51 var toPrint = expectedMiningTime; 52 Context.LogDebug(() => 53 $"expected mining time: {toPrint}, current block time: {Context.CurrentBlockTime}. " + 54 $"next: {(int) (toPrint - Context.CurrentBlockTime).Milliseconds()}"); 55 } 56 } 57 58 /// <summary> 59 /// Finally make current block time in the range of (expected_mining_time, expected_mining_time + time_for_each_block) 60 /// </summary> 61 /// <param name="miningInterval"></param> 62 /// <param name="originExpectedMiningTime"></param> 63 /// <param name="expectedMiningTime"></param> 64 private void TuneExpectedMiningTimeForTinyBlock(int miningInterval, Timestamp originExpectedMiningTime, 65 ref Timestamp expectedMiningTime) 66 { 67 var timeForEachBlock = miningInterval.Div(AEDPoSContractConstants.TotalTinySlots);//8 for now 68 var currentBlockTime = Context.CurrentBlockTime; 69 while (expectedMiningTime < currentBlockTime && 70 expectedMiningTime < originExpectedMiningTime.AddMilliseconds(miningInterval)) 71 { 72 expectedMiningTime = expectedMiningTime.AddMilliseconds(timeForEachBlock); 73 var toPrint = expectedMiningTime.Clone(); 74 Context.LogDebug(() => $"Moving to next tiny block time slot. {toPrint}"); 75 } 76 }

最后再次调整区块执行时间限制

在根据Behaviour计算出下一次生产区块的时间后,有可能会出现下一次出快时间为负数的情况(即当前时间已经超过理论上的下一次出块时间),此时可以把区块打包时间限制设置为0。最后为了给生成系统交易、网络延时等预留一定的时间,会把区块执行时间限制再乘以一个系数(待优化)。

1private void AdjustLimitMillisecondsOfMiningBlock(Round currentRound, string publicKey, 2 int nextBlockMiningLeftMilliseconds, out int limitMillisecondsOfMiningBlock) 3 { 4 var minerInRound = currentRound.RealTimeMinersInformation[publicKey]; 5 var miningInterval = currentRound.GetMiningInterval(); 6 var offset = 0; 7 if (nextBlockMiningLeftMilliseconds < 0) 8 { 9 Context.LogDebug(() => "Next block mining left milliseconds is less than 0."); 10 offset = nextBlockMiningLeftMilliseconds; 11 } 12 13 limitMillisecondsOfMiningBlock = miningInterval.Div(AEDPoSContractConstants.TotalTinySlots).Add(offset); 14 limitMillisecondsOfMiningBlock = limitMillisecondsOfMiningBlock < 0 ? 0 : limitMillisecondsOfMiningBlock; 15 16 var currentRoundStartTime = currentRound.GetStartTime(); 17 var producedTinyBlocksForPreviousRound = 18 minerInRound.ActualMiningTimes.Count(t => t < currentRoundStartTime); 19 20 if (minerInRound.ProducedTinyBlocks == AEDPoSContractConstants.TinyBlocksNumber || 21 minerInRound.ProducedTinyBlocks == 22 AEDPoSContractConstants.TinyBlocksNumber.Add(producedTinyBlocksForPreviousRound)) 23 { 24 limitMillisecondsOfMiningBlock = limitMillisecondsOfMiningBlock.Div(2); 25 } 26 else 27 { 28 limitMillisecondsOfMiningBlock = limitMillisecondsOfMiningBlock 29 .Mul(AEDPoSContractConstants.LimitBlockExecutionTimeWeight)//3 for now 30 .Div(AEDPoSContractConstants.LimitBlockExecutionTimeTotalWeight);//5 for now 31 } 32 }

以上完整代码可见:

https://github.com/AElfProject/AElf/tree/dev/contract/AElf.Contracts.Consensus.AEDPoS

可能的优化方向

基于现有逻辑优化条件,有可能的话实现成函数式,以增加代码可读性(也可能更糟)。

本文初衷只是我自己整理之前的代码,看有没有可能优化的地方,果然有一点收获,删掉了一些没有必要的判断。

如果有人能review完代码请务必告诉我。能发现任何问题就感激不尽了……

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

Java日期时间API系列31

  时间戳是指格林威治时间1970年01月01日00时00分00秒起至现在的总毫秒数,是所有时间的基础,其他时间可以通过时间戳转换得到。Java中本来已经有相关获取时间戳的方法,Java8后增加新的类Instant等专用于处理时间戳问题。 1获取时间戳的方法和性能对比1.1获取时间戳方法Java8以前

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )

AEDPoS合约实现之GetConsensusCommand - HelloWorld