正如文章AElf共識合約標準中所述,GetConsensusCommand接口用於獲取某個公鑰下一次生產區塊的時間等信息。node
在AEDPoS的實現中,其輸入僅爲一個公鑰(public key),該接口實現方法的調用時間另外做爲參考(其實也是一個重要的輸入)。AElf區塊鏈中,當系統內部調用只讀交易時,合約執行的上下文是自行構造出來的,調用時間也就是經過C#自帶函數庫的DateTime.UtcNow生成了一個時間,而後把這個時間轉化爲protobuf提供的時間戳數據類型Timestamp,傳入合約執行的上下文中。git
事實上,不管要執行的交易是否爲只讀交易,合約代碼中均可以經過Context.CurrentBlockTime來獲取當前合約執行上下文傳進來的時間戳。github
本文主要解釋AEDPoS共識如何實現GetConsensusCommand。在此以前,對不瞭解AElf共識的聚聚簡單介紹一下AEDPoS的流程。網絡
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共識合約項目。
在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時會作特殊處理。
AEDPoS合約中,要讓GetConsensusCommand方法返回ConsensusCommand,首先會根據輸入的公鑰和調用時間獲得AElfConsensusBehaviour。而後再使用AElfConsensusBehaviour判斷下一次出塊時間等信息。
這裏的邏輯相對比較清晰,也許能夠用一張圖來解釋清楚:
v2-08f91a4bf9c62c00eaf1c66da5397d4f_hd.jpg
GetConsensusBehaviour
相關完整代碼見:aelf的GitHub主頁:
https://github.com/AElfProjec...
接下來咱們逐個討論每一個Behaviour對應的ConsensusCommand。
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(); }
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();
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)); }
AElfConsensusBehaviour.NextTerm會根據當前的選舉結果從新選定17位CDC,生成新一屆第一輪的信息。方法同AElfConsensusBehaviour.NextRound不對第一屆第一輪作特殊處理的狀況。
expectedMiningTime = currentRound.ArrangeAbnormalMiningTime(minerInRound.PublicKey, Context.CurrentBlockTime); nextBlockMiningLeftMilliseconds = (int) (expectedMiningTime - Context.CurrentBlockTime).Milliseconds();
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