XMR門羅幣新算法:RandomX設計說明(翻譯,上篇)

(原文地址:https://github.com/tevador/RandomX/blob/master/doc/design.md, 總共有3章以及附錄,本篇是第1章和第2章)


       爲了最小化專用硬件的性能優勢,工作量證明(proof of work, PoW)算法必須針對現有通用硬件的特定特性實現設備綁定。這是一項複雜的任務,因爲我們必須面對大量來自不同製造商的不同架構的設備。
       有兩類不同的通用處理設備:中央處理器(CPU)和圖形處理器(GPU)。RandomX針對CPU的原因如下:

  •        CPU作爲一種不太專用的設備,更加普遍和容易接觸到。CPU綁定的算法更加平等,允許更多的參與者加入網絡。這是原始的CryptoNote白皮書[1]中陳述的目標之一。
  •        在不同架構的CPU的專有指令集中存在一個較大的公共子集。但同樣的說法就不適用於GPU了,例如,NVIDIA和AMD的GPU沒有公共的整數乘法指令[2]。
  •        所有主流的CPU指令集都有詳細的文檔,還有多種開源編譯器可用。相比之下,GPU指令集通常是私有的,可能需要供應商的特定閉源驅動程序才能獲得最佳性能。

1. 設計的考量
       設計一種CPU綁定的工作量證明算法的最基本思想是:“工作”必須是動態的。這利用了CPU接受兩種輸入的事實:數據(主輸入)和代碼(指定對數據執行什麼操作)。
       相反地,典型的密碼學哈希函數[3]不代表對CPU而言的合適工作,因爲它們唯一的輸入是數據,而操作序列是固定的,可以通過專用集成電路更高效地執行。


1.1動態工作量證明
動態工作量證明算法一般包括以下4個步驟:

  •        生成一個隨機程序。
  •        翻譯成本地CPU的機器代碼。
  •        執行程序。
  •        將程序的輸出轉換爲密碼學的安全值。

       實際“有用的”CPU綁定工作在步驟3中執行,因此必須對算法進行調優,以最小化其它步驟的開銷。


1.1.1生成隨機程序
       設計動態工作量證明的早期嘗試是基於用高級語言(如C或Javascript)生成程序[4,5]。然而,這是非常低效的,主要有兩個原因:

  •        高級語言有複雜的語法,生成一段有效的程序是相對較慢的,因爲它需要創建一棵抽象語法樹(ASL)。
  •        一旦生成了程序的源代碼,編譯器通常會將文本表示解析回ASL,這就讓生成源代碼的整個過程變得多餘。

       生成隨機程序的最快方法是使用無邏輯生成器——簡單地用隨機數據填充緩衝區。當然,這需要設計一種無語法的編程語言(或指令集),其中所有的隨機比特串都表示有效的程序。


1.1.2將程序翻譯成機器碼
       這一步是不可避免的,因爲我們不想將算法限制在一種特定的CPU架構中。爲了儘可能快地生成機器碼,我們需要指令集儘可能地接近本地硬件,同時仍然足夠通用,以支持不同的架構。在代碼編譯期間,沒有足夠的時間進行代價高昂的優化。


1.1.3執行程序
       實際的程序執行應該使用儘可能多的CPU組件。程序中應該利用的一些特性是:

  •        多級緩存(L1, L2, L3)
  •        μop緩存[6]
  •        算術邏輯單元(ALU)
  •        浮點單元(FPU)
  •        內存控制器
  •        指令級並行[7]
                  超標量執行[8]
                  亂序執行[9]
                  推測執行[10]
                  寄存器重命名[11]

       第2章描述RandomX VM如何利用這些特性。


1.1.4計算最終結果
       Blake2b[12]是一個密碼學中的安全哈希函數,它是專門爲軟件快速執行而設計的,特別是在現代64位處理器上,它的速度大約是SHA-3的三倍,並且能夠以每字節輸入消耗大約3個時鐘週期的速度運行。此函數是一個對CPU友好的工作量證明算法的理想候選者。
       爲了以密碼學中安全的方式處理大量數據,高級加密標準(AES)[13]可以提供最快的處理速度,因爲許多現代CPU支持這些操作的硬件加速。有關在RandomX中使用AES的更多細節,請參見第3章。


1.2“簡單程序問題”
       當一個隨機程序生成後,人們可以選擇只在有利的時候執行它。這項策略是可行的,主要有兩個原因:

  •        隨機生成的程序的運行時間通常遵循對數正態分佈[14](也請參閱附錄C)。生成的程序可以被快速分析,如果它可能有高於平均值的運行時間,則可以跳過此程序的執行,轉而生成一個新程序。這可以顯著地提高性能,特別是在運行時間分佈有一個沉重的尾部(許多長運行時間的異常值)和程序生成成本較低的情況下。
  •        可以選擇優化程序執行所需的特徵子集來實現。例如,可以去掉對某些操作(如除法)的支持,或者更有效地執行某些指令序列。生成的程序將會被分析,只有在符合優化實現的特定需求時纔會被執行。

       這些搜索擁有特定屬性的程序的策略偏離了工作量證明的目標,因此必須消除它們。這可以通過要求執行N個隨機程序組成的一個序列來實現,序列中每個程序都是由前一個程序的輸出生成的。最後程序的輸出就作爲結果。
                                 輸入--> 程序1 --> 程序2 -->…--> 程序(N-1) --> 程序N --> 結果
       其原理是,在第一個程序被執行後,礦工必須二選一:或者完成整條鏈(可能包括不利的程序),或者重新開始,並浪費掉在未完成的鏈上已經花費的努力。附錄A中給出了這會如何影響不同挖礦策略的哈希率的例子。
       此外,這種鏈式程序執行具有使整個鏈的運行時相等的優點,因爲一組相同分佈的運行時間總和的相對偏差減少了。


1.3驗證時間
       由於工作量證明的目的是用於缺乏信任的對等網絡,所以網絡參與者必須能夠快速驗證一個工作量證明是否有效。這就對工作量證明算法的複雜度設定了一個上限。特別地,我們爲RandomX設定了一個目標,它的驗證速度至少應與它試圖取代的CryptoNight哈希函數[15]一樣快。


1.4 內存困難
       除了純粹的計算資源(如ALU和FPU)外,CPU通常還可以訪問大量DRAM[16]形式的內存。內存子系統的性能通常被調整以匹配計算能力,例如[17]:

  •        單通道內存用於嵌入式和低功耗CPU
  •        雙通道內存用於桌面CPU
  •        三或四通道內存用於工作站CPU
  •        六或八通道內存用於高端服務器CPU

       爲了利用外部內存和芯片上的內存控制器,工作量證明算法應當訪問一個大的內存緩衝區(稱爲“數據集”)。數據集必須滿足:

  •        大於芯片上可以存儲的總量(需要外部內存)
  •        動態(需要可寫內存)

       單塊芯片上可製造的最大SRAM容量,16nm製程能超過512MiB,7nm製程能超過2GiB[18]。理想情況下,數據集的大小至少應該爲4GiB。然而,由於驗證時間的限制(見下文),RandomX使用的大小被選爲2080MiB。雖然理論上可以用目前的技術(2019年的7nm)在單塊芯片上製造此大小的SRAM,但這種解決方案的可行性值得懷疑,至少在未來短期內是這樣。


1.4.1 輕量客戶端驗證
       雖然對於解決工作量證明的專用挖礦系統,要求內存大於2GiB是合理的,但是必須爲輕量客戶端提供一個選項,以便使用少得多的內存來驗證工作量證明。
       “快速”和“輕量”模式所需的內存比例必須謹慎選擇,以使輕量模式不適合挖礦。特別是輕量模式的面積-時間(Area-Time, AT)乘積不應小於快速模式的AT乘積。AT乘積的減少量是衡量交換攻擊的常用方法[19]。(譯者注:交換攻擊是指用時間換內存空間,而內存空間正比於芯片面積。)
       根據前幾章描述的約束條件,快速和輕量驗證模式之間的最大可能的性能比被經驗性地定爲8。這是因爲:

  •        進一步增加輕量驗證時間將違反第1.3章提出的限制。
  •        進一步減少快速模式運行時間將違反第1.1章提出的約束條件,特別是程序生成和結果計算的開銷會變得過高。

      另外,256 MiB被選爲在輕客戶機模式下所需的最大內存。這個數字是可以接受的,即使是樹莓派之類的小型單板電腦。
       要保持恆定的內存時間乘積,快速模式的最大內存需求是:
                                                                  8 * 256 MiB = 2048 MiB
       這還可以進一步增加,因爲對於超標量哈希函數,輕量模式需要額外的芯片面積(參見第3.4章和第6章)。假設,保守估計每個超標量哈希核需要0.2mm2的芯片面積, DRAM的面密度爲0.149 Gb/mm2[20],則額外需要的內存爲:
                                                        8 * 0.2 * 0.149 * 1024 / 8 = 30.5 MiB
或者32 MiB,即近似到最接近的2的整數次冪。在AT乘積大致是常數的情況下,快速模式的總內存需求可以是2080MiB。

2. 虛擬機架構
    本節描述RandomX虛擬機(VM)的設計。

2.1指令集
    RandomX使用了固定長度的指令編碼,每條指令8個字節。這允許在指令字中包含一個32位的立即值。通過選擇指令字比特位的解釋,可使任意8字節的字都是有效的指令。這就允許非常高效的隨機程序生成(參見1.1.1章)。

2.1.1指令複雜性
    此VM是一臺複雜指令集機器,它同時允許寄存器和內存尋址操作。但是,每條RandomX指令僅轉換成1-7條x86指令(平均1.8條)。保持指令複雜度相對較低是很重要的,這樣可以最大限度地降低具有定製指令集的專用硬件的效率優勢。

2.2程序
    經VM執行的程序是由256條隨機指令組成的循環。

  •     256條指令已足夠多,可提供大量可能的程序和足夠的分支空間。能生成的不同程序的數量可達2^512 = 1.3e+154,這也是隨機數生成器可能的種子值的數量。
  •     256條指令又足夠少,因此,高性能CPU執行一次循環的耗時與從DRAM中獲取數據的耗時相近。這是有利的,因爲這允許數據集訪問是同步的和完全可預取的(見2.9章)。
  •     由於程序是一個循環,它可以利用存在於某些x86 CPU中的μop緩存[6]。從μop緩存中運行循環允許CPU關閉x86指令解碼器,這應該有助於均衡x86和具備同樣指令解碼器的架構之間的能效。

2.3寄存器
    VM使用8個整數寄存器和12個浮點寄存器。這是在x86-64架構中可以作爲物理寄存器分配的最大值,在常見的64位CPU架構中,x86-64架構的結構寄存器最少。使用更多的寄存器會讓x86 CPU處於劣勢,因爲它們必須使用內存來存儲VM寄存器的內容。

2.4整數運算
    RandomX使用具有高輸出熵的所有原始整數運算:加法(IADD_RS, IADD_M)、減法(ISUB_R, ISUB_M, INEG_R)、乘法(IMUL_R, IMUL_M, IMULH_M, ISMULH_R, ISMULH_M, IMUL_RCP)、異或(IXOR_R, IXOR_M)和循環移位(IROR_R, IROL_R)。

2.4.1 IADD_RS
    IADD_RS指令利用了CPU的地址計算邏輯,大多數CPU (x86 lea, ARM add)可以在一條硬件指令中執行。

2.4.2 IMUL_RCP
    由於整數除法在CPU中不是完全流水線執行的,並且在ASIC中可以更快地完成,所以IMUL_RCP指令僅要求每個程序進行一次除法來計算倒數。這就迫使ASIC必須包含一個硬件除法器,但除法器在程序執行期間又無法提供性能優勢。

2.4.3 IROR_R / IROL_R
    循環移位指令分爲右移和左移,比例爲4:1。循環右移具有更高的頻率,是因爲一些架構(如ARM)不支持循環左移(必須使用循環右移來模擬)。

2.4.4 ISWAP_R
    這條指令可被支持寄存器重命名/移動消除的CPU高效執行。

2.5浮點運算
    RandomX使用雙精度浮點運算,大多數CPU都是支持的,並且需要比單精度運算更復雜的硬件。所有運算都是作爲128位向量運算執行的,所有主流的CPU架構也都支持這種操作。
    RandomX使用IEEE 754標準保證的五種運算來給出正確的舍入結果:加法、減法、乘法、除法和平方根。標準定義的全部4種舍入模式都被用上了。

2.5.1浮點寄存器組
    浮點運算的領域被分成使用F組寄存器的“加法”運算和使用E組寄存器的“乘法”運算。這樣做是爲了防止當一個較小的數與一個較大的數相加減時,加法/減法變成“無操作”。由於F組寄存器的範圍大約被限制在±3.0e+14,因此,加或減一個絕對值大於1的浮點數總是會改變至少5個小數比特位。
    因爲F組寄存器有限的範圍會允許使用更高效的定點表示法(80bit的數),所以FSCAL指令操作浮點格式的二進制表示會使這種優化更加困難。
    E組寄存器被限制爲正值,這避免了NaN結果(如負數的平方根或0 *∞)。除法只使用內存源操作數,以避免被優化爲常數倒數的乘法。E組內存操作數的指數設定爲-255到0之間的值,以避免被0除和乘,並增加可獲得的數的範圍。E組寄存器可能值的大致範圍是1.7E-77到無窮大。
    每個程序循環結束後浮點寄存器值的近似分佈如下圖所示(左-F組,右-E組):

(注:柱狀圖中的立柱用區間的左邊值標記,如:標記爲1e-40的立柱包含從1e-40到1e-20的值)。
    F組寄存器在1e+14附近的少量數值是由FSCAL指令造成的,該指令大大增加了寄存器值的範圍。
    E組寄存器值覆蓋了一個非常大的範圍。大約2%的程序產生至少一個無窮大的值。
    爲了讓熵最大化,也爲了適應一個64字節的高速緩存行,在每次循環結束後浮點寄存器的結果被XOR運算組合,然後存儲到暫存器中。

2.6分支
    現代CPU使用了大量的芯片面積和能量來處理分支。這包括:

  •     分支預測單元[21]
  •     允許CPU在分支預測錯誤時進行恢復的檢查點/回滾狀態。

    爲了利用推測執行設計,隨機程序應該包含分支。然而,如果分支預測失敗,推測執行的指令將被丟棄,這將導致每次錯誤預測都會浪費一些能量。因此,我們應該儘量減少錯誤預測的次數。
    此外,代碼中的分支是必不可少的,因爲分支可以顯著減少靜態優化的數量。例如,考慮以下x86指令序列:

…
branch_target_00:
…
    xor r8, r9
    test r10, 2088960
    je branch_target_00
    xor r8, r9
…

    兩次XOR運算的效果通常會抵消,但由於分支的存在而不能優化掉,因爲如果執行了分支,結果將會不同。類似地,如果沒有分支,ISWAP_R指令總是可以被靜態優化去掉。
    一般來說,隨機分支必須按如下方式設計:
    1.無限循環是不可能的。
    2.預測錯誤的分支數量很少。
    3.分支條件依賴於一個運行時值,以禁用靜態分支優化。

2.6.1分支預測
    不幸的是,我們還沒有找到在RandomX中使用分支預測的方法。因爲RandomX是一個共識協議,所有的規則都必須提前設定,包括分支的規則。完全可預測的分支不能依賴於任何VM寄存器的運行時值(因爲寄存器值是僞隨機的和不可預測的),所以它們必須是靜態的,因而可以通過專門的硬件輕鬆地進行優化。

2.6.2 CBRANCH指令
    因此,RandomX使用跳轉概率爲1/256的隨機分支和依賴於整數寄存器值的條件分支,這些分支將被CPU預測爲“不會執行”。在大多數CPU設計中,這樣的分支是“空閒的”,除非它們被執行。雖然這並沒有利用分支預測器,但與非推測性分支處理相比,推測性設計將獲得顯著的性能提升。更多信息請參見附錄B。
    在選擇分支條件和跳轉目標時,必須滿足RandomX代碼中不可能存在無限循環,因爲控制分支的寄存器在重複代碼塊中永遠不會被修改。每條CBRANCH指令可以連續跳轉兩次。使用預測執行[22]來處理CBRANCH是不切實際的,因爲大多數情況下該分支不會執行。

2.7指令級並行
    CPU使用一些技術來提高它們的性能,這些技術利用了執行代碼的指令級並行性。這些技術包括:

  •     擁有多個可以並行執行操作的執行單元(超標量執行)。
  •     不按程序順序執行指令,而是按操作數的可用性(亂序執行)。
  •     預測分支將以何種方式執行,以增加超標量和亂序執行的益處。

    RandomX可從這些優化中獲益。詳細分析見附錄B。

2.8 暫存器
    暫存器被用作可讀寫內存。它的大小被選爲完全匹配CPU緩存。

2.8.1 暫存器級別
    暫存器分爲3個級別,用於模擬典型的CPU緩存層次結構[23]。大多數VM指令訪問“L1”和“L2”暫存器,因爲L1和L2緩存位於CPU執行單元附近,提供最低的隨機訪問延遲。從L1和L2中讀取數據的比例是3:1,這與典型延遲時間的反比相匹配(見下表)。

CPU μ-architecture L1 延遲 L2 延遲 L3 延遲 資料來源
ARM Cortex 2 6 - [24]
AMD Zen+ 4 12 40 [25]
Intel Skylake 4 12 42 [26]

    L3緩存要大得多,並且位於離CPU核心更遠的地方。因此,它的訪問延遲要高得多,並可能導致程序執行的暫停。
    因此,RandomX在每個程序循環中只執行兩次對“L3”暫存器的隨機訪問(參見第4.6.2章中的步驟2和步驟3)。來自給定循環的寄存器值被寫入與加載它們相同的位置,這就保證了所需的高速緩存行已被移動到速度更快的L1或L2緩存中。
    此外,從固定地址讀取的整數指令也會使用整個“L3”暫存器(參看表5.1.4),因爲重複的訪問將確保高速緩存行被放置在CPU的L1緩存中。這表明,暫存器級別並不總是直接對應於相同的CPU緩存級別。

2.8.2暫存器寫入
    在VM執行過程中,有兩種方法可以修改暫存器:
    1.在每次程序循環結束時,所有寄存器值都被寫入“L3”暫存器(參見第4.6.2章,步驟9和步驟11)。每次循環將在兩個64字節的塊中總計寫入128字節。
    2.ISTORE指令執行顯式存儲。平均每個程序有16次存儲,其中2次存儲會進入“L3”級別。每條ISTORE指令寫入8個字節。
    下圖展示了一個暫存器寫入數據分佈情況的例子。圖像中的每個像素代表暫存器中的8個字節。紅色像素表示在哈希計算期間至少被覆蓋了一次的暫存器部分。“L1”和“L2”級位於左側(幾乎完全被覆蓋)。暫存器的右側代表底部的1792KiB。只有大約66%的部分被覆蓋,但這些寫入分佈得均勻且隨機。

關於暫存器熵的分析見附錄D。

2.8.3讀寫比例
    每個程序循環對暫存器平均執行39次讀取(IADD_M、ISUB_M、IMUL_M、IMULH_M、ISMULH_M、IXOR_M、FADD_M、FSUB_M、FDIV_M)和16次寫入(ISTORE)。另外128個字節被隱式地讀寫以初始化和存儲寄存器值。每次循環從數據集讀取64字節的數據。總計:

  •     每次程序循環從內存中讀取的平均數據量是:39 * 8 + 128 + 64 = 504字節
  •     每次程序循環向內存中寫入的平均數據量是:16 * 8 + 128 = 256字節

    這接近2:1的讀/寫比例,也是CPU優化的目標。

2.9數據集
    由於暫存器通常存儲在CPU緩存中,所以只有訪問數據集時才使用內存控制器。
    RandomX在每個程序循環中隨機讀取數據集一次(每個哈希結果讀取16384次)。由於數據集必須存儲在DRAM中,於是它提供了一種自然的並行化限制,因爲在每個存儲庫組中,DRAM無法執行超過2500萬次/秒的隨機訪問。每個單獨可尋址的存儲庫組允許大約1500Hash/s的吞吐量。
    所有數據集訪問都讀取一CPU高速緩存行(64字節),並且是完全預取的。第4.6.2章中描述的執行一個程序循環的時間與典型的DRAM訪問延遲(50-100 ns)大致相同。

2.9.1緩存
    用於輕量驗證和數據集構建的緩存近似是數據集的1/8大。爲保持恆定的面積-時間乘積,每個數據集項由8個隨機緩存訪問構成。
    因爲256MiB足夠小,可以包含在芯片上,RandomX使用自定義的高延遲、高能耗的混合函數(“超標量哈希”),這抵消了使用低延遲內存的好處,並且計算超標量哈希所需的能量使得輕量模式挖礦非常低效(參見3.4章)。
    由於將抗交換攻擊的Argon2d函數迭代了3次,使用少於256MiB的內存是不可能的。當使用3次迭代時,內存使用量減半將增加3423倍的計算成本,才能實現最佳的交換攻擊[27]。

(原文中只看到三張和附錄A~F,並未看到第4章以及其它章節。)


參考資料:
References
[1] CryptoNote whitepaper - https://cryptonote.org/whitepaper.pdf
[2] ProgPoW: Inefficient integer multiplications - https://github.com/ifdefelse/ProgPOW/issues/16
[3] Cryptographic Hashing function - https://en.wikipedia.org/wiki/Cryptographic_hash_function
[4] randprog - https://github.com/hyc/randprog
[5] RandomJS - https://github.com/tevador/RandomJS
[6] μop cache - https://en.wikipedia.org/wiki/CPU_cache#Micro-operation_(%CE%BCop_or_uop)_cache
[7] Instruction-level parallelism - https://en.wikipedia.org/wiki/Instruction-level_parallelism
[8] Superscalar processor - https://en.wikipedia.org/wiki/Superscalar_processor
[9] Out-of-order execution - https://en.wikipedia.org/wiki/Out-of-order_execution
[10] Speculative execution - https://en.wikipedia.org/wiki/Speculative_execution
[11] Register renaming - https://en.wikipedia.org/wiki/Register_renaming
[12] Blake2 hashing function - https://blake2.net/
[13] Advanced Encryption Standard - https://en.wikipedia.org/wiki/Advanced_Encryption_Standard
[14] Log-normal distribution - https://en.wikipedia.org/wiki/Log-normal_distribution
[15] CryptoNight hash function - https://cryptonote.org/cns/cns008.txt
[16] Dynamic random-access memory - https://en.wikipedia.org/wiki/Dynamic_random-access_memory
[17] Multi-channel memory architecture - https://en.wikipedia.org/wiki/Multi-channel_memory_architecture
[18] Obelisk GRN1 chip details - https://www.grin-forum.org/t/obelisk-grn1-chip-details/4571
[19] Biryukov et al.: Tradeoff Cryptanalysis of Memory-Hard Functions - https://eprint.iacr.org/2015/227.pdf
[20] SK Hynix 20nm DRAM density - http://en.thelec.kr/news/articleView.html?idxno=20
[21] Branch predictor - https://en.wikipedia.org/wiki/Branch_predictor
[22] Predication - https://en.wikipedia.org/wiki/Predication_(computer_architecture)
[23] CPU cache - https://en.wikipedia.org/wiki/CPU_cache
[24] Cortex-A55 Microarchitecture - https://www.anandtech.com/show/11441/dynamiq-and-arms-new-cpus-cortex-a75-a55/4
[25] AMD Zen+ Microarchitecture - https://en.wikichip.org/wiki/amd/microarchitectures/zen%2B#Memory_Hierarchy
[26] Intel Skylake Microarchitecture - https://en.wikichip.org/wiki/intel/microarchitectures/skylake_(client)#Memory_Hierarchy
[27] Biryukov et al.: Fast and Tradeoff-Resilient Memory-Hard Functions for Cryptocurrencies and Password Hashing - https://eprint.iacr.org/2015/430.pdf Table 2, page 8
[28] J. Daemen, V. Rijmen: AES Proposal: Rijndael - https://csrc.nist.gov/csrc/media/projects/cryptographic-standards-and-guidelines/documents/aes-development/rijndael-ammended.pdf page 28
[29] 7-Zip File archiver - https://www.7-zip.org/
[30] TestU01 library - http://simul.iro.umontreal.ca/testu01/tu01.html

發佈了44 篇原創文章 · 獲贊 96 · 訪問量 35萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章