1. 程式人生 > >區塊鏈的隨機數之傷

區塊鏈的隨機數之傷

你好,我是不羈,一名程式設計師,帶你玩轉EOS智慧合約開發。如果你對EOS智慧合約感興趣,歡迎關注我的專欄。

簡介:昨天我在為什麼EOSBet不敢開源?一文中,和大家探討了EOS上隨機數的生成問題。隨機數的生成問題的確是很多區塊鏈專案的硬傷,也是不少專案不願意開源的原因之一。今天我們繼續探討怎樣生成隨機數,既能保證公平,又能讓黑客無法根據隨機過程提前預測隨機結果,從而避免專案方利益受損;同時本文會引出一個目前業界用的比較成熟的方案(其實仍稍欠完美,本文也會解析)。

我們昨天留了這麼個問題:

學過C++的都知道,向系統請求開闢一塊記憶體時,該記憶體的地址是不確定的,那你覺得可以利用這個特點,在智慧合約中生成隨機數嗎?

不知道你有沒有寫個試驗程式碼試試呢?不羈做過實驗,實驗過程就不在這裡演示了。
這個問題的答案是,雖然記憶體的地址生成我們是不確定的,但對於虛擬機器來說是確定的,在它每次實行時,相應執行路徑上的變數的地址都是一樣的。這是可以想見的,因為每次執行action,虛擬機器都是初始化一個新的執行沙盒環境,相同的action,執行過程也必然相同,中間沒有任何不確定的因素擾動執行過程,從而執行過程也都是可以確定的。這樣黑客把你的程式碼完全拿過來,然後打印出相關變數的地址,就可以確定了。而因為每次執行都一樣,所以黑客在本地列印的結果也就是你在主網上執行時相關變數的地址。
可見,利用隨機開闢的記憶體地址作為隨機種子

也是不可取的。

隨機種子

我剛才用到了隨機種子這個詞,什麼是隨機種子呢?因為隨機過程是計算機程式,程式分兩種,一種是無狀態的,一種是有狀態的。
無狀態的程式,每次對於相同的輸入總是給出相同的輸出,也就是說,只要你給我一個輸入,我都可以私下把程式執行一下,然後給你一個結果,你自己再執行一次,也必要是這個結果。
有狀態的程式,雖然每次的執行結果可能不同,但只要你告訴我初始狀態,我也可以根據你每次給的輸入,預測出它的輸出。
嚴格來數,所有的隨機過程都是有狀態的計算機程式。而隨機種子呢,就相當於它的初始狀態,或者可對它的當前狀態產生一個不可預測的擾動,變成另外一個狀態。
所以假如你想讓你的隨機數是不可預測的,你必須保證隨機種子是不可預測的。
這樣我們就可以把產生隨機數的機理分成兩部分了,一部分是隨機種子,一部分是隨機過程。只要隨機種子不公開,把隨機過程公開,那麼黑客也就無法對產生的隨機數進行預測了。隨機過程可以寫在智慧合約裡,隨機種子就需要外界的輸入了,並且是不可預測的外界輸入。只要隨機種子是不可預測的,那麼隨機結果也就是不可預測的了。

我前面討論過,用時間和wasm虛擬機器分配的記憶體地址都是可以預測的,儘管預測的難度有點技術大,但還是能預測的,所以都不是良好的隨機種子。
什麼是良好的隨機種子呢?因為虛擬機器執行過程的確定性,所以這個隨機種子我們不能從虛擬機器中來找了,只能從外界來找了。

利用區塊中的資料作為隨機種子

昨天有一位同學在留言中提到了這個想法,他是這麼說的:

 

Screen Shot 2018-09-22 at 09.21.43.png

他提到使用當前區塊的hash值作為隨機種子,這是很好的想法。
目前EOS的實際使用的tps平均在16左右,一般情況下,每個區塊都不止一個交易,也就是說,黑客發起的攻擊性交易總會伴隨著其他的交易,而其他的交易資料是黑客無法預測的,所以用當前區塊的hash值作為隨機種子是比較優良的隨機種子

然而,據我目前所知,當前EOS智慧合約中還沒有獲得當前區塊的hash值的介面,如果你發現了,歡迎告訴我。倒是有獲取當前action所在的transaction的資料,對於dice合約而言,這個東西黑客就是可以預測的了,因為交易是由黑客主動發起的,它完全知道自己的資料。

EOS智慧合約中,還有獲得當前區塊的blocknumber以及當前交易的ref block的介面,不過這些也不能作為隨機種子,前者是可以預測的,後者是黑客可以自己指定的。

一個可能的方案

經過我們的分析發現,目前在EOS上,好的隨機種子無法在智慧合約內部獲得,那麼只能求助外界了。我們看看外界目前有什麼公共的或許可用的不可預測的噪音資料,我列出瞭如下幾條:

  • RAM的交易資料
  • 某些活躍度比較高的dapps公開的資料

這些資料的確是不可預測的,任何一秒鐘都可能發生變化,並且變化的方式是極難預測的,他們可以作為隨機種子的一部分,但如果單純依靠它們中的任何一個,都是不夠可靠的。以RAM的交易資料為例,交易往往幾秒鐘才有一次,變化頻次不夠,黑客完全可以在這個變化的空檔期下手。最活躍的賬號,和活躍的dapps公開的資料也是如此,不過如果把它們結合起來一起用作隨機種子,可靠性會高很多。

不過這種方式也有缺點:

  1. 太依賴於別人的公開資料了,如果別人的資料格式變了,你的智慧合約可能也要跟著更新
  2. 如果這些公開的資料變化的頻率,有時高,有時低,你必須收集足夠多的頻繁變化公開資料才能確保每時每刻的不可預測性
  3. 目前EOS上滿足條件的可作為隨機種子使用的公開資料仍然比較少

成熟可靠的方案

我們仍然需要沿著前面的思路,好的隨機種子只能從我們自己的智慧合約以外去尋找。我們發現其他的dapps的智慧合約的公開的、並且變化的資料可以作為隨機種子,但因為現階段EOS上的生態還在起步階段,可以作為隨機種子的資料還是太少,可靠性不足。

那怎麼辦呢?讓遊戲的參與方提供隨機種子。

dice遊戲為例,雙方同時提供隨機種子,然後智慧合約把這兩個種子一起作運算,這個運算演算法是公開的。因為雙方是同時提供的,所以任何一方都無法提前預知對方的種子是什麼,自然無法對結果做出預測。
“可是不行啊,同時提供種子,這在實操中很難辦到啊!”
是的,的確如此。不過可以用另外一個方法來解決這個問題。
EOSIO提供了一個dice的示例合約,它是兩個玩家一起玩,不像EOSbet那種是玩家和專案方玩的。

我們先看看EOSIO提供的dice示例的玩法:

  1. 使用者A和B各有一個自己的種子,分別是seedAseedB,但一開始互相保密的。
  2. A對seedA進行hash運算,生成seedA_hash,然後把seedA_hash發給智慧合約。
  3. B對seedB進行hash運算,生成seedB_hash,然後把seedB_hash發給智慧合約。
  4. 在上面的步驟完成之後,A把seedA傳送給智慧合約
  5. B把seedB傳送給智慧合約
  6. 智慧合約先驗證A和B的seed,驗證通過後,再對兩個seed進行運算,根據運算結果判定輸贏。

在1,2之後,B雖然先看到了A的seedA_hash,但因為hash運算的性質,B無法通過seedA_hash計算出seedA,而智慧合約的運算過程是對雙方的seed進行的,所以B此時無法通過調整自己的seed來影響運算結果的傾向性。於是B也只能老老實實的生成一個隨機的seedB,並進行hash運算。

在5步的時候,B看到了seedA,自己還沒有傳送seedB,那這個時候他可以調整seedB來影響結果了嗎?不行,因為第6步,智慧合約會對seed進行有效性驗證,具體是判斷這個等式是否成立:

hash(seed) == seed_hash 

如果不成立,就代表這個驗證失敗了。seed_hash是玩家在第2和3步已經發送給智慧合約了的。同樣因為hash的性質,你想生成另外一個seed同時還能滿足上面的等式,是相當困難對策。

通過這種方式,dice的玩家雙方就可以不用顧慮誰先出示seed_hash了,也不用擔心誰先出示seed了,這個過程巧妙的利用了hash運算的性質:

  1. 很難根據hash(seed)的結果倒推出seed
  2. 不同的seed進行hash之後,生成的結果極大概率是不同。這個極大概率無限接近於100%

hash運算在加密領域應用非常廣泛,並且有多種不同的hash演算法,這裡就不展開了。

改進的版本

上面的方案中,玩家雙方都各生成了一個隨機種子參與運算,所以任何一方都無法提前預測隨機結果,對於雙方都是公平的。

我們假設A、B雙方中,A是使用者,B是專案方。專案方提供智慧合約,並開源,然後採用上面的玩法,可以嗎?

完全可行,B的相關操作可以根據A的請求自動執行,但A的操作就變複雜了。
dice合約為例,對於A而言,最好的體驗是像EOSBet那樣,使用者只需要進行一次操作,即可得到輸贏的結果。然而在上面的方案中,A卻需要兩步:第一步是向合約傳送seed_hash,第二步是向合約傳送seed

如何做到使用者A只需要一步合約操作就可以得到結果呢?
方案是有的,我們再回頭看看上面的過程,第2步和第3步是可以替換的,像下面這樣:

  1. 使用者A和B各有一個自己的種子,分別是seedAseedB,但一開始互相保密的。
  2. B對seedB進行hash運算,生成seedB_hash,然後把seedB_hash發給智慧合約。
  3. A對seedA進行hash運算,生成seedA_hash,然後把seedA_hash發給智慧合約。
  4. 在上面的步驟完成之後,A把seedA傳送給智慧合約
  5. B把seedB傳送給智慧合約
  6. 智慧合約先驗證A和B的seed,驗證通過後,再對兩個seed進行運算,根據運算結果判定輸贏。

我們可以看到第3步和第4步是緊挨著的,於是我們就可以把它合成一個操作了。變成一個步驟之後,seedA_hash就不用提供了,因為seedA_hash本身是為了校驗seedA的,現在在這個一次操作已經給了seedA,那麼seedA_hash就沒有存在的必要的。
如此,改進後的版本就變成下面這樣:

  1. 使用者A和B各有一個自己的種子,分別是seedAseedB,但一開始互相保密的。
  2. B對seedB進行hash運算,生成seedB_hash,然後把seedB_hash發給智慧合約。
  3. A把seedA傳送給智慧合約
  4. B把seedB傳送給智慧合約
  5. 智慧合約先驗證B的seedB是否合法,驗證通過後,再對兩個seed進行運算,根據運算結果判定輸贏。

如此就實現了使用者只需要一次智慧合約互動即可,但專案方B還是需要做兩次智慧合約互動,能不能再改進呢?

能的。
專案方B可以把seedB_hash交給A,並由A在與智慧合約的互動中帶上。於是,整個過程就變成這樣了:

  1. 使用者A和B各有一個自己的種子,分別是seedAseedB,但一開始互相保密的。
  2. B對seedB進行hash運算,生成seedB_hash,然後把seedB_hash交給A。
  3. A把seedA以及seedB_hash傳送給智慧合約
  4. B把seedB傳送給智慧合約
  5. 智慧合約先驗證B的seedB是否合法,驗證通過後,再對兩個seed進行運算,根據運算結果判定輸贏。

這樣A和B與智慧合約就分別只有一次互動了(省了點鏈上的資源消耗),而是在A和B之間增加了一次互動(鏈下的),很容易看出這次互動是安全的。
對於dice合約來說,每次的遊戲都是玩家發起的,也就是A發起的,而專案方B則是被動的,所以當A在把seedA傳送給智慧合約之前,本身就需要通知B去生成seedBseedB_hash,於是B就可以在收到A的通知的時候,順便把seedB_hash返回給A。

EOSBet或許也是這麼做的,但因為它沒開源,我們不敢定論。不過倒是有一個開源的仿EOSBet的dapps: fairdicegame,它的合約原始碼在這裡。這個專案使用的就是改進後的方案,不過因為它涉及到需要讓玩家A知道B是預先生成的seedB,所以它會把seedB_hash預先發送給A,由A在與智慧合約的互動中,把seedB_hash也帶上。
首先宣告一下,我和該專案沒有任何關係,卻無意間替他們做了宣傳,我把他們的合約程式碼地址放在這裡,主要是因為比較欣賞他們這種把合約開源的態度,同時也是為了方便智慧合約的開發者們學習。

還能再改進嗎?

上面方案從使用者使用的角度已經能夠滿足需要了,同時也保證了公平公正,但在自證清白方面不夠徹底。因為在玩家操作期間,客戶端除了和智慧合約有互動外,客戶端還需要服務端進行一次互動,主要是為了在為玩家生成seed以前,獲取服務端的seed_hash,然後把服務
智慧合約程式碼已經公開了,可以自證清白,但服務端就很難了。

fairdicegame為例,首先它的服務端沒有開源,其次,即便它開源了也無法自證清白,因為沒辦法驗證它開源的版本與實際使用的版本是同一個版本。智慧合約因為可以進行原始碼驗證,所以沒有這個問題。

我看了一下fairdicegame與服務端互動的資料,從幾次的試驗來看,客戶端是在收到服務端的seedB_hash之後,再把自己的seedAseedB_hash傳送給服務端的,所以服務端在生成seedB的時候,是不知道seedA的,從而可以認為fairdicegame對於玩家來說還是比較公平的。不過因為客戶端的程式碼和服務端程式碼都完全是由專案方控制的,假如專案方不定時的給了使用者另一個不公正的版本,使用者也很難感知到。

你可能會說:“你是不是太嚴格了點兒?”

是的,的確是嚴格了點,我只有不停的嚴格要求自己,才能做出讓使用者更加可信的dapps。

另一方面,這種方式產生隨機數的方式,嚴格依賴於雙方共同參與隨機數的生成過程,儘管某些程式可以對互動邏輯進行優化,但還是比較複雜,這種複雜性也增加了隨機數生成場景的通用性,比如區塊鏈遊戲中生成寶物的隨機演算法,完全是官方隨機生成的,整個過程無需使用者參與,上面適用於dice的隨機數生成機制在這裡就不合適了。


作者:鵬飛_3870
連結:https://www.jianshu.com/p/3d3d9e53b799
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。