【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
相關文章
相關標籤/搜索