近期,我們宣布了Neoverse NFT收集活動,通過開盲盒生成N3元素碎片,而合成9款不同的元素碎片,即可合成一款由國際知名NFT藝術家專門打造的N3典藏版NFT。

那麼這項收集活動的玩法設計邏輯是什麼?共9款N3典藏版的生成規則又是怎樣的呢?

這篇文章中,NGD參與此次活動的開發者將與大家分享Neoverse NFT算法設計和合約開發的一些有趣細節。

算法設計

  1. 開盲盒遊戲怎麼玩?

一共有27000個盲盒,一個盲盒的價格為2GAS,買十送二。開盲盒的意思是將盲盒銷毀,隨機獲得N3元素碎片。碎片共有9種,各自編號獨立,都是從1到3000。例如Fragment A #733、Fragment I #2314等。

用戶合成全部9種碎片後,可以獲得一個N3典藏版NFT,該典藏版NFT的種類由合成它的9種碎片的序列號之和決定。序列號之和小於4816則為N系列,大於4816且小於6411為E系列,其餘為O系列。

  1. 為什麼是9個碎片合成一個NFT?

九種碎片分別代表了Neo的9個屬性:Interoperability, Native oracles, Self-sovereign ID, Decentralized storage, Neo name service, One block finality, Best-in-class tooling, Smart contracts, Multi-language。

九種N3元素碎片合成一個N3典藏版NFT,體現了Neo“All in ONE”的理念。

  1. 4816和6411是怎麼算出來的?

在討論碎片背後的數學邏輯之前,我們先介紹一下其市場邏輯。

一級市場:碎片將通過空投和拍賣進行隨機分發。

二級市場:碎片可以在NFT交易平台上交易。

其中,市場活躍度的大小將會直接影響湊出的碎片序列號之和的大小。根據算法模擬,在市場交換最充分的時候,N系列號為900;在市場交換最不充分的時候,N系列號為8731。因此,如果我們假設市場活躍度為中等,則其均值為4816。同理,可以算出E系列號的均值為6411。

合約開發

盲盒遊戲最重要的部分是隨機性。既然涉及到隨機性,一定有人問你們的隨機性是怎麼實現的,是否公平,能不能被預測,能不能被黑客利用等。

我們通過以下幾個版本的合約來逐步分析,一步步找到最佳的開盲盒方案。

青銅版本:

在開盲盒的時候,取當前區塊的Nonce(在N3的區塊中,有一個字段叫Nonce,它是隨機的,但是是固定不變的),作為隨機數種子,再對3000取模+1,獲得1~3000的隨機數。

這樣操作看起來很簡單直接,但是存在很多問題:

1、同一個區塊所開的盲盒都是相同的結果

2、取出的隨機數可能有重複

白銀版本:

在這個版本中,針對上個版本的1號問題進行了修復。首先想到的是將區塊的Nonce和交易ID進行異或操作,獲得隨機數的種子,但這樣在一筆交易中開出的盲盒又是相同的結果,顯然也是不行的。然後想到對盲盒的TokenId進行哈希運算,將其轉為大整數,並與當前區塊進行異或操作,獲得隨機數的種子。因為每個盲盒的TokenId是不同的,哈希自然也是不同的,與Nonce進行異或操作,可避免每次都產生同一個隨機數。但是NGD工程師黎工表示直接使用當前區塊的Nonce會有一些被人利用的風險:

1、黑客可以發布一個合約A,在合約A中調用開盲盒的方法,並且對開盲盒的結果進行判斷,如果發現開盲盒的結果不滿意則拋出異常,中斷合約執行。

2、黑客可以通過在開盲盒的腳本後面追加精心構造的OpCode,完成上面的步驟。

這兩點在前一版本中也同樣存在。

黃金版本:

在這個版本里,我們將隨機數放在買盲盒之後,開盲盒之前。這樣當你買盲盒的時候無法預測將來的區塊Nonce,當你開盲盒的時候,結果是確定的。

具體操作如下。購買盲盒時,記錄下下一個區塊的索引。當你開盲盒時,取那個區塊的Nonce並和TokenId的哈希進行異或。

這樣就避免了黑客的攻擊,但是針對青銅版本的第二個問題,仍沒有解決。兩個盲盒可以開出同樣的碎片,比如Fragment A #33

鉑金版本:

在這個版本里,我們主要解決隨機數重複的問題,有幾種解決方案:

1、存儲區中存儲所有未被使用的隨機數(最多27000個),每用到一個,就將其刪除。下個人取隨機數時,讀取存儲區中的隨機數數組,從中取隨機數。

2、存儲區中存儲所有已使用的隨機數(最多27000個),每取一個隨機數,就將其存到存儲區裡。下個人取隨機數時,讀取存儲區中的隨機數數組,找到未使用的隨機數,從中取隨機數。

以上兩種方案其實是等價的,無論存儲區中存儲已使用的,還是未使用的隨機數,都要大量使用存儲區。按每個隨機數2字節計算,一共54KB的數據,寫入一次約13.5GAS,手續費是相當高昂的。

那麼如果用位(Bit)存儲呢,一個長度為3375的字節數組,每一位的下標表示隨機數本身,每一位的值(0或1)表示該隨機數是否使用。再結合分片操作,將9種碎片的隨機數分開存儲。這樣讀取寫入的費用會大大降低,但考慮到將位還原為數組,會有大量計算,手續費仍然不很樂觀。 NGD工程師印工提出了一種新的解決方案。針對每種碎片的1~3000的隨機數,存儲區中存儲最後一個隨機數的下標,和抽取替換的過程。具體來說,假設用戶抽取了下標為500的隨機數,則把500給他,並在存儲區中記錄k:500,v:3000,表示下標500的位置存放的是隨機數3000。然後將隨機數下標的最大值3000更改為2999。第二個用戶又抽取到下標為500的隨機數,檢查存儲區,將下標500位置的隨機數3000給用戶,並更新存儲區k:500,v:2999。然後將隨機數下標的最大值2999更改為2998。依此類推。

隨機數生成的描述為:第一個人,Nonce模3000+1第二個人,Nonce模2999+1;第三個人,Nonce模2998+1;

細心的用戶又會發現,在黃金版本中,開盲盒的結果是確定的,不隨著你開的先後順序而變化,但在這個版本中,你先開和後開,結果可能會變化。這樣又暴漏了另一個問題:

1、黑客可以在本地對合約進行預執行,如果結果滿意,則廣播交易,如果不滿意,則等待其它人開出該類碎片後再進行預執行操作,直到它滿意了,發送交易。

2、白銀版本中的兩個問題又出現了。

星鑽版本:

到這裡我就不得不吹一下Neo底層中的隨機數生成算法的厲害之處了。其實這個隨機數算法是在N3上線前加入的,我在寫前幾個版本的合約的時候還不能使用。 NGD工程師劉工給我介紹了這個隨機數算法。它能保證真隨機、不可預測(預執行和鏈上執行的隨機數不同)、每使用一次就會更改。在合約中也可以很方便地用互操作服務(Runtime.GetRandom())來使用這個隨機數。結合鉑金版本中印工的方案,即可每次獲得1-3000中不重複的隨機數。到這裡,隨機數部分已經近乎完美了。但是白銀版本中提到的兩個問題是真正在鏈上執行,只是增加對結果的判斷,這是隨機數算法所不能干預的,要通過其它安全限制來規避。我計算了一下,黑客通過這種方式開盲盒的成本比較高,初始成本為10.1GAS,之後每嘗試開一次盲盒成本為1.05GAS。嘗試兩次都不滿意的話,相當於直接浪費了一個盲盒(1個盲盒的價格為2GAS)。但即使這樣,即使黑客虧本攻擊,我們仍然希望他無從下手,所以有了下一個版本。

皇冠版本:

我和Neo社區工程師廖工在星鑽版本上進行了改進。

1、在這個版本中添加了對合約調用的限制,只允許通過交易來調用Neoverse合約進行開盲盒操作,不允許通過其它合約進行調用。

2、針對開單個盲盒以及批量開盲盒的方法,根據實參長度精確計算所需腳本長度。不允許在標準調用腳本中附加任何一個字節的腳本。

到此合約近乎完美。