【AELF开发者社区】AEDPoS合约实现之GetConsensusCommand

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

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

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

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

AEDPoS Process

DPoS的基本概念咱们再也不赘述,假设如今AElf主链经过投票选举出17个节点,咱们(暂时地)称之为AElf Core Data Center,简称CDC。(对应eos中的BP即Block Producer这个概念。)数据结构

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

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

Round的数据结构以下:区块链

// The information of a round.
message Round {优化

sint64 round_number = 1;
map<string, MinerInRound> real_time_miners_information = 2;
sint64 main_chain_miners_round_number = 3;
sint64 blockchain_age = 4;
string extra_block_producer_of_previous_round = 7;
sint64 term_number = 8;

}this

// The information of a miner in a specific round.
message MinerInRound {

sint32 order = 1;
bool is_extra_block_producer = 2;
aelf.Hash in_value = 3;
aelf.Hash out_value = 4;
aelf.Hash signature = 5;
google.protobuf.Timestamp expected_mining_time = 6;
sint64 produced_blocks = 7;
sint64 missed_time_slots = 8;
string public_key = 9;
aelf.Hash previous_in_value = 12;
sint32 supposed_order_of_next_round = 13;
sint32 final_order_of_next_round = 14;
repeated google.protobuf.Timestamp actual_mining_times = 15;// Miners must fill actual mining time when they do the mining.
map<string, bytes> encrypted_in_values = 16;
map<string, bytes> decrypted_previous_inValues = 17;
sint32 produced_tiny_blocks = 18;

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

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

ConsensusCommand

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

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;

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

message AElfConsensusHint {

AElfConsensusBehaviour behaviour = 1;

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

enum AElfConsensusBehaviour {

UpdateValue = 0;
NextRound = 1;
NextTerm = 2;
UpdateValueWithoutPreviousInValue = 3;
Nothing = 4;
TinyBlock = 5;

}

逐一解释:

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判断下一次出块时间等信息。

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

v2-08f91a4bf9c62c00eaf1c66da5397d4f_hd.jpg

GetConsensusBehaviour

相关完整代码见:aelf的GitHub主页:

https://github.com/AElfProjec...

接下来咱们逐个讨论每一个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。

if (currentRound.RoundNumber == 1)
{
    // To avoid initial miners fork so fast at the very beginning of current chain.
    nextBlockMiningLeftMilliseconds =
        currentRound.GetMiningOrder(publicKey).Mul(currentRound.GetMiningInterval());
    expectedMiningTime = Context.CurrentBlockTime.AddMilliseconds(nextBlockMiningLeftMilliseconds);
}
else
{
    // As normal as case AElfConsensusBehaviour.UpdateValue.
    expectedMiningTime = currentRound.GetExpectedMiningTime(publicKey);
    nextBlockMiningLeftMilliseconds = (int) (expectedMiningTime - Context.CurrentBlockTime).Milliseconds();
}

GetConsensusCommand - UpdateValue

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

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

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

GetConsensusCommand - NextRound

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

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

不然,为了防止指定的额外区块生产者掉线或者在另一个分叉上出块(在网络不稳定的状况下会出现分叉),其余全部的CDC也会获得一个互不相同额外区块生产的时间槽,这些CDC在同步到任何一个CDC生产的额外区块后,会马上重置本身的调度器,因此没必要担忧产生冲突。

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

...
var minerInRound = currentRound.RealTimeMinersInformation[publicKey];
if (currentRound.RoundNumber == 1)
{
    nextBlockMiningLeftMilliseconds = minerInRound.Order.Add(currentRound.RealTimeMinersInformation.Count).Sub(1)
            .Mul(currentRound.GetMiningInterval());
    expectedMiningTime = Context.CurrentBlockTime.AddMilliseconds(nextBlockMiningLeftMilliseconds);
}
else
{
    expectedMiningTime =
        currentRound.ArrangeAbnormalMiningTime(minerInRound.PublicKey, Context.CurrentBlockTime);
    nextBlockMiningLeftMilliseconds = (int) (expectedMiningTime - Context.CurrentBlockTime).Milliseconds();
}
...

/// <summary>
/// If one node produced block this round or missed his time slot,
/// whatever how long he missed, we can give him a consensus command with new time slot
/// to produce a block (for terminating current round and start new round).
/// The schedule generated by this command will be cancelled
/// if this node executed blocks from other nodes.
/// </summary>
/// <returns></returns>
public Timestamp ArrangeAbnormalMiningTime(string publicKey, Timestamp currentBlockTime)
{
    if (!RealTimeMinersInformation.ContainsKey(publicKey))
    {
        return new Timestamp {Seconds = long.MaxValue};
    }

    miningInterval = GetMiningInterval();

    if (miningInterval <= 0)
    {
        // Due to incorrect round information.
        return new Timestamp {Seconds = long.MaxValue};
    }

    var minerInRound = RealTimeMinersInformation[publicKey];

    if (GetExtraBlockProducerInformation().PublicKey == publicKey)
    {
        var distance = (GetExtraBlockMiningTime().AddMilliseconds(miningInterval) - currentBlockTime).Milliseconds();
        if (distance > 0)
        {
            return GetExtraBlockMiningTime();
        }
    }

    var distanceToRoundStartTime = (currentBlockTime - GetStartTime()).Milliseconds();
    var missedRoundsCount = distanceToRoundStartTime.Div(TotalMilliseconds(miningInterval));
    var expectedEndTime = GetExpectedEndTime(missedRoundsCount, miningInterval);
    return expectedEndTime.AddMilliseconds(minerInRound.Order.Mul(miningInterval));
}

GetConsensusCommand - NextTerm

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

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

GetConsensusCommand - TinyBlock

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

当前CDC为上一轮的额外区块生产者,在生产完包含NextRound交易的区块之后,须要在同一个时间槽里继续生产最多7个区块;
当前CDC刚刚生产过包含UpdateValue交易的区块,须要在同一个时间槽继续生产最多7个区块。
基本判断逻辑是,若是当前CDC为本轮出过包含UpdateValue交易的块,即状况2,就结合当前CDC是上一轮额外区块生产者的状况,把一个长度为4000ms的时间槽切分红8个500ms的小块时间槽,进行分配;不然为上述的状况1,直接根据已经出过的小块的数量分配一个合理的小块时间槽。

/// <summary>
/// We have 2 cases of producing tiny blocks:
/// 1. After generating information of next round (producing extra block)
/// 2. After publishing out value (producing normal block)
/// </summary>
/// <param name="currentRound"></param>
/// <param name="publicKey"></param>
/// <param name="nextBlockMiningLeftMilliseconds"></param>
/// <param name="expectedMiningTime"></param>
private void GetScheduleForTinyBlock(Round currentRound, string publicKey,
    out int nextBlockMiningLeftMilliseconds, out Timestamp expectedMiningTime)
{
    var minerInRound = currentRound.RealTimeMinersInformation[publicKey];
    var producedTinyBlocks = minerInRound.ProducedTinyBlocks;
    var currentRoundStartTime = currentRound.GetStartTime();
    var producedTinyBlocksForPreviousRound =
        minerInRound.ActualMiningTimes.Count(t => t < currentRoundStartTime);
    var miningInterval = currentRound.GetMiningInterval();
    var timeForEachBlock = miningInterval.Div(AEDPoSContractConstants.TotalTinySlots);//8 for now
    expectedMiningTime = currentRound.GetExpectedMiningTime(publicKey);

    if (minerInRound.IsMinedBlockForCurrentRound())
    {
        // After publishing out value (producing normal block)
        expectedMiningTime = expectedMiningTime.AddMilliseconds(
            currentRound.ExtraBlockProducerOfPreviousRound != publicKey
                ? producedTinyBlocks.Mul(timeForEachBlock)
                // Previous extra block producer can produce double tiny blocks at most.
                : producedTinyBlocks.Sub(producedTinyBlocksForPreviousRound).Mul(timeForEachBlock));
    }
    else if (TryToGetPreviousRoundInformation(out _))
    {
        // After generating information of next round (producing extra block)
        expectedMiningTime = currentRound.GetStartTime().AddMilliseconds(-miningInterval)
            .AddMilliseconds(producedTinyBlocks.Mul(timeForEachBlock));
    }

    if (currentRound.RoundNumber == 1 ||
        currentRound.RoundNumber == 2 && !minerInRound.IsMinedBlockForCurrentRound())
    {
        nextBlockMiningLeftMilliseconds = GetNextBlockMiningLeftMillisecondsForFirstRound(minerInRound, miningInterval);
    }
    else
    {
        TuneExpectedMiningTimeForTinyBlock(miningInterval,
            currentRound.GetExpectedMiningTime(publicKey),
            ref expectedMiningTime);

        nextBlockMiningLeftMilliseconds = (int) (expectedMiningTime - Context.CurrentBlockTime).Milliseconds();

        var toPrint = expectedMiningTime;
        Context.LogDebug(() =>
            $"expected mining time: {toPrint}, current block time: {Context.CurrentBlockTime}. " +
            $"next: {(int) (toPrint - Context.CurrentBlockTime).Milliseconds()}");
    }
}

/// <summary>
/// Finally make current block time in the range of (expected_mining_time, expected_mining_time + time_for_each_block)
/// </summary>
/// <param name="miningInterval"></param>
/// <param name="originExpectedMiningTime"></param>
/// <param name="expectedMiningTime"></param>
private void TuneExpectedMiningTimeForTinyBlock(int miningInterval, Timestamp originExpectedMiningTime,
    ref Timestamp expectedMiningTime)
{
    var timeForEachBlock = miningInterval.Div(AEDPoSContractConstants.TotalTinySlots);//8 for now
    var currentBlockTime = Context.CurrentBlockTime;
    while (expectedMiningTime < currentBlockTime &&
           expectedMiningTime < originExpectedMiningTime.AddMilliseconds(miningInterval))
    {
        expectedMiningTime = expectedMiningTime.AddMilliseconds(timeForEachBlock);
        var toPrint = expectedMiningTime.Clone();
        Context.LogDebug(() => $"Moving to next tiny block time slot. {toPrint}");
    }
}

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

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

private void AdjustLimitMillisecondsOfMiningBlock(Round currentRound, string publicKey,

int nextBlockMiningLeftMilliseconds, out int limitMillisecondsOfMiningBlock)
{
    var minerInRound = currentRound.RealTimeMinersInformation[publicKey];
    var miningInterval = currentRound.GetMiningInterval();
    var offset = 0;
    if (nextBlockMiningLeftMilliseconds < 0)
    {
        Context.LogDebug(() => "Next block mining left milliseconds is less than 0.");
        offset = nextBlockMiningLeftMilliseconds;
    }

    limitMillisecondsOfMiningBlock = miningInterval.Div(AEDPoSContractConstants.TotalTinySlots).Add(offset);
    limitMillisecondsOfMiningBlock = limitMillisecondsOfMiningBlock < 0 ? 0 : limitMillisecondsOfMiningBlock;

    var currentRoundStartTime = currentRound.GetStartTime();
    var producedTinyBlocksForPreviousRound =
        minerInRound.ActualMiningTimes.Count(t => t < currentRoundStartTime);

    if (minerInRound.ProducedTinyBlocks == AEDPoSContractConstants.TinyBlocksNumber ||
        minerInRound.ProducedTinyBlocks ==
        AEDPoSContractConstants.TinyBlocksNumber.Add(producedTinyBlocksForPreviousRound))
    {
        limitMillisecondsOfMiningBlock = limitMillisecondsOfMiningBlock.Div(2);
    }
    else
    {
        limitMillisecondsOfMiningBlock = limitMillisecondsOfMiningBlock
            .Mul(AEDPoSContractConstants.LimitBlockExecutionTimeWeight)//3 for now
            .Div(AEDPoSContractConstants.LimitBlockExecutionTimeTotalWeight);//5 for now
    }
}

以上完整代码可见:

https://github.com/AElfProjec...

可能的优化方向

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

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

若是有人能review完代码请务必告诉我。能发现任何问题就感激涕零了……

-----aelf开发者社区:EanCuznaivy
相关文章
相关标签/搜索