本文主要討論區塊鏈系統中隨機數的常見方案,AElf中對於可提供隨機數的智能合約提供的標準接口,以及AEDPoS合約對ACS6的實現。算法
關於ACS的說明可見這篇文章的開頭。segmentfault
區塊鏈系統中,與合約相關的隨機數應用大體有幾種場景:抽獎、驗證碼、密碼相關等。緩存
而因爲區塊鏈本質上是一個分佈式系統,他要求各個節點的運算結果是可驗證的,傳統的隨機數生成結果在不一樣機器上基本不會一致,讓全部的節點產生一樣的隨機數又不形成過多的延時是不可能的。安全
好在區塊鏈系統中生成一個可用的隨機數,咱們已知有幾種方案。數據結構
中心化生成隨機數。隨機數由可信的第三方提供,如RANDOM.ORG。
Commitment方案,或者hash-commit-reveal方案。若是讀者有讀過AElf白皮書會發現AElf主鏈共識用於肯定每一輪區塊生產者肯定生產順序的in_value和out_value便採用了這種方案:區塊生產者在本地生成一個隨機的哈希值in_value後,先公佈承諾(即out_value),其中out_value = hash(in_value),到了合適的時機再公佈隨機哈希值in_value,其餘節點只須要驗證out_value == hash(in_value)就能夠了。這裏的in_value能夠認爲是一個隨機數。
採集區塊鏈狀態信息做爲種子,在合約中生成隨機數。萬一被人知道了隨機數的生成算法(智能合約的代碼是公開的),再獲取到正確的種子,這個方案生成的隨機數就能夠成功預測的。不敢相信還真有人用這種方式。框架
顯然,站在去中心化的角度上考量,Commitment方案至少是一個可用的方案,只須要保證做出承諾(commitment)的人不會本身偷偷提早公開隨機數,或者本身利用隨機數做弊便可。dom
然而很不幸,其實在區塊鏈系統中,這是沒法保證的:咱們沒法保證生成隨機數的人不會利用信息不對等來作出不公平的事情,好比當這個隨機數被做爲某次賭局開獎的依據時,隨機數的生成者哪怕在賭局開始以前就作出了承諾,依然能夠選擇性地停止公開這個隨機數——這樣至關於他獲得了「再玩一次」的機會,由於若是他不公開這個隨機數,要麼賭局會選擇其餘人公開的隨機數,要麼這個賭局會做廢。async
若是預防隨機數生產者的選擇停止攻擊呢?有一系列成熟的方案,參看Secret Sharing。分佈式
簡單解釋一下:如今有五我的A~E,每人掌握一個公私鑰對,此時A產生了一個隨機數Random,生成對應的承諾Commitment,同時將隨機數Random與B、C、D、E的公鑰進行加密獲得四個SharingPart,加密SharingPart時便保證只須要湊夠B~E中兩我的就能夠恢復Random,將SharingPart和Commitment一塊兒公開。這樣哪怕他本身因故沒有公開Random的值,B~E中任意兩我的用本身的私鑰分別對本身收到的SharingPart解密,湊齊兩個解密後的數值(要按A加密出SharingPart的順序),即可以恢復出Random。而萬一兩個SharingPart沒能恢復出Random,只能認爲A從一開始就決定做惡——這時候只須要在區塊鏈經濟系統的設計中,讓A付出代價就好了。好比直接扣除A申請稱爲隨機數生產者繳納的保證金。(TODO: 畫圖)ide
此外,咱們還能夠選擇不依賴某一個個體產生的隨機數,而是選擇多個個體的隨機數進一步計算哈希值做爲應用場景中的可用隨機數。如此咱們能夠比較穩定、安全地在區塊鏈系統中獲得隨機數。
我在以前的一篇文章中解釋了AElf區塊鏈關於共識的合約標準,其實是做爲AElf主鏈開發者對合約開發者實現共識機制時推薦實現的接口。而關於隨機數生成,咱們制定了ACS6,做爲對任何提供隨機數的合約推薦要實現的接口。
不出意外地,ACS6是選擇對Commitment方案進行抽象,獲得的合約標準。
支持使用ACS6的場景以下:
用戶對實現了ACS6的合約申請一個隨機數,相似於發送了一個定單;
實現了ACS6的合約給用戶返回一些信息,這些信息包括用戶能夠在哪一個區塊高度(H)獲取獲得一個隨機數,以及用戶獲取隨機數可用的憑據T(也是一個哈希值);
等待區塊鏈高度到達指定高度H後,用戶發送交易嘗試獲取隨機數,憑據T須要做爲該交易的參數;
實現了ACS6的合約根據憑據T返回一個隨機數。
若是用戶嘗試在高度H以前獲取隨機數,本次獲取隨機數的交易會執行失敗並拋出一個AssertionException,提示高度還沒到。
基於以上場景,咱們設計的ACS6以下:
service RandomNumberProviderContract {
rpc RequestRandomNumber (google.protobuf.Empty) returns (RandomNumberOrder) { } rpc GetRandomNumber (aelf.Hash) returns (aelf.Hash) { }
}
message RandomNumberOrder {
sint64 block_height = 1;// Orderer can get a random number after this height. aelf.Hash token_hash = 2;
}
用戶發送RequestRandomNumber交易來申請一個隨機數,合約須要爲本次請求生成一個憑據(token_hash),而後把該憑據和用戶可以獲取該隨機數的區塊高度一塊兒返回給用戶。高度達到之後,用戶利用收到的憑據(token_hash)發送GetRandomNumber交易便可獲得一個可用的隨機數。做爲合約,在實現該方法的時候應該緩存爲用戶生成的憑據,做爲一個Map的key,這個Map的value則應該根據合約本身對隨機數的實現自行定義數據結構。
好比,AEDPoS合約在實現ACS6的時候,能夠將該Map的value定義爲:
message RandomNumberRequestInformation {
sint64 round_number = 1; sint64 order = 2; sint64 expected_block_height = 3;
}
其中round_number指示爲了生成該用戶申請的隨機數,應該使用哪一輪(及以後)各個CDC公佈的previous_in_value值;order爲這個用戶申請隨機數的RandomNumberProviderContract交易被該輪第幾個CDC打包(因此須要使用該輪該次序之後公佈的previous_in_value做爲隨機數生成的「原材料」);expected_block_height則是要告知給用戶的須要等待到的區塊高度。
因爲AEDPoS共識自己的推動過程當中就採用了hash-commit-reveal的方式,能夠直接使用每一個CDC的產生普通區塊(區別於額外區塊)時公佈的previous_in_value來做爲生成隨機數的原材料。新公佈的previous_in_value的驗證(即驗證hash(previous_in_value) == previous_out_value)發生於任何節點執行新的區塊以前,只要該區塊被成功同步上best chain,就無需擔憂已經公佈的previous_in_value存在弄虛做假的行爲。
注意,本節的實現只是在剛剛定義好ACS6後的一個嘗試,以後隨時可能會有大幅度改動。
惟一要擔憂的是CDC共謀。所以AEDPoS在實現隨機數生成的時候,不會僅僅採用一個CDC的previous_in_value來生成隨機數,而是設置了五個:
public static class AEDPoSContractConstants
{
... public const int RandomNumberRequestMinersCount = 5;
}
當一個用戶向AEDPoS請求隨機數時,他只能等到當前輪的最後一個時間槽(額外區塊時間槽),才能夠百分之百保證本輪可公佈的previous_in_value都已經公佈了。由於可能存在本輪剛剛加入(也許是剛解決完分叉)的CDC,他沒有previous_in_value可公佈;也可能存在一些CDC因爲本地緩存問題,未能公佈previous_in_value,而到了額外區塊的時間槽時,他的previous_in_value隨着secret sharing的reveal階段完成被恢復——若是咱們容許用戶在reveal階段以前發送GetRandomNumber交易獲取隨機數,那麼在reveal階段以前和以後獲取到的隨機數可能會不一致,這會很使人困擾。
/// <summary>
/// In AEDPoS, we calculate next several continual previous_in_values to provide random hash.
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public override RandomNumberOrder RequestRandomNumber(Empty input)
{
var tokenHash = Context.TransactionId; if (TryToGetCurrentRoundInformation(out var currentRound)) { var lastMinedBlockMinerInformation = currentRound.RealTimeMinersInformation.Values.OrderBy(i => i.Order) .LastOrDefault(i => i.OutValue != null); var lastMinedBlockSlotOrder = lastMinedBlockMinerInformation?.Order ?? 0; var minersCount = currentRound.RealTimeMinersInformation.Count; // At most need to wait one round. var waitingBlocks = minersCount.Sub(lastMinedBlockSlotOrder).Add(1).Mul(AEDPoSContractConstants.TinyBlocksNumber); var expectedBlockHeight = Context.CurrentHeight.Add(waitingBlocks); State.RandomNumberInformationMap[tokenHash] = new RandomNumberRequestInformation { RoundNumber = currentRound.RoundNumber, Order = lastMinedBlockSlotOrder, ExpectedBlockHeight = expectedBlockHeight }; return new RandomNumberOrder { BlockHeight = expectedBlockHeight, TokenHash = tokenHash }; } Assert(false, "Failed to get current round information"); // Won't reach here. return new RandomNumberOrder { BlockHeight = long.MaxValue };
}
用戶使用憑據試圖獲取隨機數時,AEDPoS會收集用戶申請隨機數後五個由CDC公佈的previous_in_value,用這五個哈希值計算出來一個哈希值,做爲一個可用的隨機數返回給用戶。
public override Hash GetRandomNumber(Hash input)
{
var roundNumberRequestInformation = State.RandomNumberInformationMap[input]; if (roundNumberRequestInformation == null) { Assert(false, "Random number token not found."); // Won't reach here. return Hash.Empty; } if (roundNumberRequestInformation.ExpectedBlockHeight > Context.CurrentHeight) { Assert(false, "Still preparing random number."); } var targetRoundNumber = roundNumberRequestInformation.RoundNumber; if (TryToGetRoundInformation(targetRoundNumber, out var targetRound)) { var neededParticipatorCount = Math.Min(AEDPoSContractConstants.RandomNumberRequestMinersCount, targetRound.RealTimeMinersInformation.Count); var participators = targetRound.RealTimeMinersInformation.Values.Where(i => i.Order > roundNumberRequestInformation.Order && i.PreviousInValue != null).ToList(); var roundNumber = targetRoundNumber; TryToGetRoundNumber(out var currentRoundNumber); while (participators.Count < neededParticipatorCount && roundNumber <= currentRoundNumber) { roundNumber++; if (TryToGetRoundInformation(roundNumber, out var round)) { var newParticipators = round.RealTimeMinersInformation.Values.OrderBy(i => i.Order) .Where(i => i.PreviousInValue != null).ToList(); var stillNeed = neededParticipatorCount - participators.Count; participators.AddRange(newParticipators.Count > stillNeed ? newParticipators.Take(stillNeed) : newParticipators); } else { Assert(false, "Still preparing random number, try later."); } } // Now we can delete this token_hash from RandomNumberInformationMap // TODO: Set null if deleting key supported. State.RandomNumberInformationMap[input] = new RandomNumberRequestInformation(); var inValues = participators.Select(i => i.PreviousInValue).ToList(); var randomHash = inValues.First(); randomHash = inValues.Skip(1).Aggregate(randomHash, Hash.FromTwoHashes); return randomHash; } Assert(false, "Still preparing random number, try later."); // Won't reach here. return Hash.Empty;
}
接下來對這兩個方法添加BVT測試用例。(關於AElf合約的測試有一個框架來着,能夠用代碼生成器生成一個可以調用某個合約方法的Stub,該Stub就能夠模擬一個區塊鏈上用戶發交易,每一個交易會單獨打包爲一個區塊。以後有時間寫文章詳細介紹一下。)
在鏈剛剛啓動的時候,由任意一個模擬用戶的Stub發送RequestRandomNumber交易,檢查能不能獲得一個可用的RandomNumberOrder便可。因爲在測試用例執行以前經過大量交易(目前是19個)部署必備的合約,所以在執行該RequestRandomNumber時,區塊高度已經達到了20。
[Fact]
internal async Task<Hash> AEDPoSContract_RequestRandomNumber()
{
var randomNumberOrder = (await AEDPoSContractStub.RequestRandomNumber.SendAsync(new Empty())).Output; randomNumberOrder.TokenHash.ShouldNotBeNull(); randomNumberOrder.BlockHeight.ShouldBeGreaterThan( AEDPoSContractTestConstants.InitialMinersCount.Mul(AEDPoSContractTestConstants.TinySlots)); return randomNumberOrder.TokenHash;
}
等到必定的高度後(正好一輪時間),模擬用戶發送GetRandomNumber交易來獲取隨機數。如下的測試用例模擬第一輪的CDC進行正常生產區塊的操做,也分別在目標高度(ExpectedBlockHeight)先後嘗試發送GetRandomNumber。在高度沒有達到時,交易執行結果爲失敗,錯誤信息中包含「Still preparing random number.」;第二次發送GetRandomNumber時,交易執行成功並返回可用的隨機數值;第三次發送GetRandomNumber,由於相關隨機數的信息已經被AEDPoS合約刪除(節省空間),所以返回一個空的哈希值。
[Fact]
internal async Task AEDPoSContract_GetRandomNumber()
{
var tokenHash = await AEDPoSContract_RequestRandomNumber(); var currentRound = await BootMiner.GetCurrentRoundInformation.CallAsync(new Empty()); var randomHashes = Enumerable.Range(0, AEDPoSContractTestConstants.InitialMinersCount).Select(_ => Hash.Generate()).ToList(); var triggers = Enumerable.Range(0, AEDPoSContractTestConstants.InitialMinersCount).Select(i => new AElfConsensusTriggerInformation { PublicKey = ByteString.CopyFrom(InitialMinersKeyPairs[i].PublicKey), RandomHash = randomHashes[i] }).ToDictionary(t => t.PublicKey.ToHex(), t => t); // Exactly one round except extra block time slot. foreach (var minerInRound in currentRound.RealTimeMinersInformation.Values.OrderBy(m => m.Order)) { var currentKeyPair = InitialMinersKeyPairs.First(p => p.PublicKey.ToHex() == minerInRound.PublicKey); KeyPairProvider.SetKeyPair(currentKeyPair); BlockTimeProvider.SetBlockTime(minerInRound.ExpectedMiningTime); var tester = GetAEDPoSContractStub(currentKeyPair); var headerInformation = (await tester.GetInformationToUpdateConsensus.CallAsync(triggers[minerInRound.PublicKey] .ToBytesValue())).ToConsensusHeaderInformation(); // Update consensus information. var toUpdate = headerInformation.Round.ExtractInformationToUpdateConsensus(minerInRound.PublicKey); await tester.UpdateValue.SendAsync(toUpdate); for (var i = 0; i < 8; i++) { await tester.UpdateTinyBlockInformation.SendAsync(new TinyBlockInput { ActualMiningTime = TimestampHelper.GetUtcNow(), RoundId = currentRound.RoundId }); } } // Not enough. { var transactionResult = (await AEDPoSContractStub.GetRandomNumber.SendAsync(tokenHash)).TransactionResult; transactionResult.Error.ShouldContain("Still preparing random number."); } // In test framework, the execution of every transaction will generate a new block no matter the execution succeeded. await AEDPoSContractStub.GetRandomNumber.SendAsync(tokenHash); // Now it's enough. { var randomNumber = (await AEDPoSContractStub.GetRandomNumber.SendAsync(tokenHash)).Output; randomNumber.Value.ShouldNotBeEmpty(); } // Then this order deleted from state. { var randomNumber = (await AEDPoSContractStub.GetRandomNumber.SendAsync(tokenHash)).Output; randomNumber.Value.ShouldBeEmpty(); }
}
歡迎來找茬。