天天看點

如何調用以太坊預編譯合約?【Solidity教程】

以太坊包含了一些用于密碼學計算的預編譯合約,可以用來實作進階隐私保護功能。在這個教程中我們将了解以太坊提供的預編譯合約清單,并通過

bn256ScalarMul

bigModExp

這兩個執行個體學習以太坊預編譯合約的使用方法。

用自己熟悉的語言學習以太坊DApp開發: Java | Php Python .Net / C# Golang Node.JS Flutter / Dart

1、以太坊虛拟機基本概念

在繼續下面的教程之前,我們需要對以太坊和Solidity有一些基本的了解。我們關心的重點在于,以太坊有一個分布式的虛拟機即EVM,EVM提供了一組指令可以用于在區塊鍊上執行交易并更新狀态。關于EVM的一些基本概念如下:

  • storage:可以永久在鍊上存儲資訊
  • memory:EVM虛拟機的工作記憶體,用于儲存計算過程中的變量内容
  • uint:uint256類型的别名,可儲存256位,完美比對橢圓曲線坐标的要求
  • public:用來聲明函數位公開可調用
  • view:用來告訴編譯器,所裝飾的函數不會修改合約狀态
  • pure:表示所裝飾的函數不涉及合約狀态的讀寫

2、以太坊預編譯合約清單

以太坊Geth用戶端的預編譯合約清單看起來像這樣:

var PrecompiledContractsByzantium = map[common.Address]PrecompiledContract{     
  common.BytesToAddress([]byte{1}): &ecrecover{}, 
  common.BytesToAddress([]byte{2}): &sha256hash{}, 
  common.BytesToAddress([]byte{3}): &ripemd160hash{}, 
  common.BytesToAddress([]byte{4}): &dataCopy{}, 
  common.BytesToAddress([]byte{5}): &bigModExp{}, 
  common.BytesToAddress([]byte{6}): &bn256Add{}, 
  common.BytesToAddress([]byte{7}): &bn256ScalarMul{}, 
  common.BytesToAddress([]byte{8}): &bn256Pairing{},
}           

上述代碼中的映射結構記錄了預編譯合約的位址,是最後4個是新增的預編譯

合約:

__bigModExp__:位址0x05,執行操作:b^e mod m。bigModExp預編譯合約的輸入為:

底數長度、指數長度、模長度、底數即b的值、指數即e的值、模即m的值

__bn256Add__:位址0x06,執行操作:(x1, y1) + (x2, y2),其中x1, y1, x2, y2 都是256位的域成員,是以 (x1, y1)和 (x2, y2)都是bn256曲線上的有效點,滿足公式

y^2 = x^3 + 3 mod fieldOrder

。 bn256預編譯合約的輸入就是x1, y1, x2, y2。

__bn256ScalarMul__:位址0x07,執行操作:k * (x, y),其中k屬于群,(x,y)是曲線上的有效點。 bn256scalarMul的輸入是x, y, k。

__bn256Pairing__:位址0x08,執行操作:配對檢查

e(g1, g2) = e(-h1, h2

,其中g1和h1屬于群G1,

g2和h2屬于群G2。bn256Pairing可以接收任意多對橢圓曲線上的點。群G1上的點形式為(x,y),群

G2上的點形式為(ai + b, ci + d),其中a, b, c, d (依次為虛部、實部、虛部、實部) 需要在預編譯 調用時傳入。bn256Pairing代碼首先檢查已經送出6的倍數個成員,然後執行配對檢查。

x, y, a, b, c, d的值都是域成員,是以都會按域大小取模。在bn256ScalarMul中使用的k的值,則是按橢圓曲線群的階取模。

下面我們将要學習兩個主要的示例:bn256ScalarMul和bigModExp。bn256ScalarMul操作和bn256Add非常類似,而bn256Pairing操作則更像bigModExp,因為這兩者都接受可變長度的輸入,是以調用時需要指定輸入大小。下面是調用bn256ScalarMul的代碼:

function ecmul(uint ax, uint ay, uint k) public view returns(uint[2] memory p) { uint[3] memory input;
 input[0] = ax;
 input[1] = ay;
 input[2] = k;

 assembly {
   if iszero(staticcall(gas, 0x07, input, 0x60, p, 0x40)) {
       revert(0,0)
   }
 }
 return p;
}           

目前内聯彙編已經支援if語句,調用時設定gas數量也比以前簡單 —— 在調用時使用gas,就表示利用所有可用gas,這避免了我們自己猜測需要的gas數量。

revert操作碼将復原所有的狀态變化,起作用是在gas不足時或對預編譯合約的調用發生故障後,可以復原部分完成的狀态更新。

3、調用bn256ScalarMul預編譯合約

每個位址關聯的持久化記憶體被稱為存儲(Storage),這時一個key-value庫,實作從256位到256位資料的映射。在合約内這個鍵值庫沒有辦法枚舉,合約也不能通路其他位址關聯的存儲。

如果采用如下形式初始化變量:

uint256 blah

,那麼就會将變量blah儲存到持久化存儲。uint是uint256的别名,如果需要更細粒度的管理,可以使用uint8,uint16等等。

EVM有一個虛拟棧可以儲存256位的值。選擇256位的目的是與密碼學操作保持相容。所有的EVM操作都是利用這個虛拟棧完成的,它最多可以容納1024個成員。你可以拷貝棧頂16個成員之一,或者兩兩交換。所有其他的操作碼都利用棧頂特定位置的成員作為輸入并将結果壓入棧。

對于每一個消息調用,易失記憶體都被複位,記憶體以32位元組為機關配置設定,使用gas支付記憶體利用的成本。我們需要調用預編譯合約的值保持在這個記憶體的頂部。

我們可以将之前儲存在持久化存儲中的變量指派給記憶體,方式如下:

uint256[2] memory inputToPrecompile;
input[0] = somePreviouslyStoredValue;
input[1] = someOtherPreviouslyStoredValue;           

這實際上就是我們在ecmul中的開始4行的操作。我們将值ax,ay,k壓入虛拟棧的頂部。然後通過調用bn256ScalarMul預編譯合約的位址就完成調用了。看下一部分的代碼:

assembly {
   if iszero(staticcall(gas, 0x07, input, 0x60, p, 0x40)) {
       revert(0,0)
   }
 }           

staticcall操作碼的調用形式如下:

staticcall(gasLimit, to, inputOffset, inputSize, outputOffset, outputSize)           

可以看到在上面的調用bn256ScalarMul的代碼中,我們:

  • 在扣除2000後,發送目前可用的gas
  • 調用位址0x07的預編譯合約,這對應bn256ScalarMul
  • 使用記憶體變量input作為輸入偏移參數
  • 将輸入大小聲明為0x60,這對應3個256位數值,表示一個橢圓曲線點和一個256位标量
  • 将輸出儲存在p中
  • 輸出大小為0x40,對應要傳回的橢圓曲線點

這樣就完成了對以太坊預編譯合約bn256ScalarMul的調用,ecmul函數的傳回值現在就是bn256ScalarMul預編譯合約的傳回值!

4、調用bigModExp預編譯合約

下面的代碼調用bigModExp預編譯合約:

function expmod(uint base, uint e, uint m) public view returns (uint o) {
  
  assembly {
   // define pointer
   let p := mload(0x40)
   // store data assembly-favouring ways
   mstore(p, 0x20)             // Length of Base
   mstore(add(p, 0x20), 0x20)  // Length of Exponent
   mstore(add(p, 0x40), 0x20)  // Length of Modulus
   mstore(add(p, 0x60), base)  // Base
   mstore(add(p, 0x80), e)     // Exponent
   mstore(add(p, 0xa0), m)     // Modulus
   if iszero(staticcall(sub(gas, 2000), 0x05, p, 0xc0, p, 0x20)) {
     revert(0, 0)
   }
   // data
   o := mload(p)
  }}           

需要注意的是,0x40始終是空閑記憶體,是以可以使用

p:=mload(0x40)

來初始化記憶體指針。

原文連結:

以太坊預編譯合約使用教程 — 彙智網