以太坊 Constantinople 硬分叉內容介紹

距離上次的 Byzantium 硬分叉已滿一年,上次延後的難度炸彈正在倒數中。

Byzantium 硬分叉:


“landscape photo of high rise building” by Jensen Low on Unsplash

Constantinopole 預計於區塊 708000(2019/1/16 )進行主鏈的分叉。

此次硬分叉除了新的 CREATE2 opcode,並沒有帶來太多讓人振奮的新東西。

Constantinopole 硬分叉所包含的 EIP 如下:

  • EIP 145: 在 EVM 裡新增 Bitwise shifting 的 opcode
  • EIP 1014:新增 CREATE2 opcode
  • EIP 1052:新增 EXTCODEHASH opcode
  • EIP 1283:調整 SSTORE opcode 的計價方式
  • EIP 1234:延後難度炸彈並調降區塊獎賞

以下沒有按照號碼而是從簡單的 EIP 開始介紹。

EIP 1234:延後難度炸彈並調降區塊獎賞

基本上和上次的拆解差不多— 延後一年並調降獎賞。

其中有三個提案:

  1. 將獎賞由原本的 3 ETH 降到 1 ETH
  2. 將獎賞降到 2 ETH
  3. 維持 3 ETH 但調整 uncle reward 並移除 nephew reward 以藉此降低 ETH 發行量。

最後由第二個方案勝出。

EIP 1052:新增 EXTCODEHASH opcode

由於很多情況在合約裡會限制使用者必須是單純的帳戶(External Owned Account,EOA)或是一個合約,這時我們需要去檢查該地址是不是一個合約。或是希望和我們進行互動的合約要符合特定的模式,從而避免和未知、惡意的合約來往,這時我們需要去取回該地址的合約並檢查。

如果是要單純檢查該地址是不是合約,則使用 EXTCODESIZE 這個 opcode 來取得指定地址的合約程式碼大小,若回傳結果不為零,代表該地址是一個合約。
如果要檢查合約內容是不是我們預期的,則透過 EXTCODECOPY 這個 opcode 來取回指定地址的合約程式碼,把回傳結果拿去做雜湊,並把雜湊結果和我們事先存在合約裡的雜湊值做比對。

EXTCODESIZE 花費 700 gas,而 EXTCODECOPY 除了基本的 700 gas 之外,還要加收把合約程式碼複製到記憶體的成本(每四個 byte 收 3 gas),所以若是對方合約程式碼非常龐大,則每次要做這個檢查都要額外再花費不少 gas。

因此提出了 EXTCODEHASH opcode,直接回傳指定地址的合約程式碼的雜湊值。
其 opcode 值為 0x3F,固定花費 400 gas,如果指定位址不存在(即在鏈上沒任何資料),則回傳 0;如果存在但是是單純的帳戶(EOA),則回傳對 empty data 做雜湊的結果。

EIP 145: 在 EVM 裡新增 Bitwise shifting 的 opcode

目前 EVM 裡沒有原生的左右移運算子,而是要靠其他算術運算子組合來模擬(雖然在 Solidity 裡有平移運算的寫法,但實際編譯的 bytecode 是用指數搭配乘法/除法來模擬的)。而每一次的模擬共需要花費 35 gas。

這個 EIP 新增了左移( SHL)、邏輯右移(SHR)和算術右移(SAR)三個指令,三個 opcode 的值分別為 0x1b 0x1c 0x1d ,每個運算都只需花費 3 gas。

EIP 1283:調整 SSTORE opcode 的計價方式

這個 EIP 是以 EIP 1087 為基礎做的改進,目的都是讓某些情況下使用 storage 的成本能更合理、更便宜。

目前 SSTORE opcode 的計價方式是

  1. 從 0 設為非 0 的值:20000 gas
  2. 從非 0 修改為非 0:5000 gas
  3. 從非 0 改為 0:-10000 gas(即退還)

如果一個交易裡對同一個 storage slot 做了多次改動,則以目前的計價方式,每一次改動都會被收費,即便交易完成後它們才會被一次寫入磁碟裡(對 storage 做的改動只有在交易成功完成後才會寫入磁碟裡)。

所以假設你的交易在過程中

  • 對某個原本為 0 的 storage slot 做了 5 次修改,則你會被收取 20000 + 5 * 5000 = 45000 gas
  • 或是把某個 storage slot 從 0 改為非 0,又從非 0 改為 0(例如 Mutex 的使用),則你會被收取 20000 + 5000 – 10000 = 15000 gas
  • 或是做了兩次的代幣交換,把代幣從 A 身上轉到 B,再轉到 C 身上,則你會被收取 5000 * 4 = 20000 gas

EIP 1087 的做法

在每筆交易執行的時候,建立一個暫時的對應表來記錄每個被修改到的 storage slot,交易的最後再一次結算(看是收費還是退費)。

EIP 1283 的做法

在每一次 SSTORE 執行時,比對 storage slot 的
 (1) 原始值(即交易執行前這個 storage slot 的值)
(2) 目前的值(即交易執行到一半,當下該 storage slot 的值,可能和原始值相同,也可能不同)
 (3) 新的值(即這個 SSTORE 動作會賦予該 storage slot 的值) 
如果需要退還 gas,則當下就會記錄該退還多少,而不是等到交易的最後。

兩者的不同

EIP 1087 和 EIP 1283 的不同在於,EIP 1087 是以整個交易為一個單位,建立一個對應表,交易執行的過程中每個 SSTORE 執行時會來對這個表做修改,最後再做結算;而 EIP 1283 則是以每個 SSTORE 為一個單位,有可能當下這個 SSTORE 就是該筆交易最後一個 SSTORE 了,也有可能不是,但每個 SSTORE 執行完都會去做結算的動作。

而 EIP 1283 的優點在於不需要額外建立並維護一個對應表,而且不需要對原本客戶端的 SSTORE 代碼做太大修改,只需要新增一些判斷式而已。

註:兩個做法在每次 SSTORE 執行時都會收取最基本的 200 gas(看修改的內容和原始值而定,有可能是收取 5000 gas 或 20000 gas,但最少就是 200 gas)。

將新的計價方式套用在上面的例子,假設你的交易在過程中

  • 對某個原本為 0 的 storage slot 做了 5 次修改,則你會被收取 20000 + 5 * 200 = 21000 gas
  • 或是把某個 storage slot 從 0 改為非 0,又從非 0 改為 0,則你會被收取 20000 + 200 – 19800= 400 gas
  • 或是做了兩次的代幣交換,把代幣從 A 身上轉到 B,再轉到 C 身上,則你會被收取 5000 * 3 + 200 – 4800= 10400 gas

EIP 1014:新增 CREATE2 opcode

原本 Metropolis 規劃的新功能 — 帳戶抽象化(Account Abstraction)除了 CREATE2 opcode 本身,還包含了 ENTRY POINT(是一個帳戶,地址是 0xfff…fff,可以把它視為系統的代理人)。

Account Abstraction

在 Account Abstraction 的世界中,沒有 EOA,每個帳戶都是一個合約。每個人需要為自己建立一個合約來處理交易的細節(而不再是由系統去幫你處理):這個合約要持有足夠的以太幣(或代幣)以支付交易手續費、要驗證合約主人的身份、要記錄並檢查 nonce 值(也可以不做 nonce 值檢查)、要選擇使用哪種簽章或加密演算法(而不是像現在只能用 ECDSA)。

註:需要 nonce 值來避免交易重放攻擊(transaction replay attack)的使用者可以自己在合約裡實現這個功能:在合約內自己記錄一個 nonce 值、或甚至多個。

假設今天你要產生一筆用來發送代幣的交易,這筆交易的發送者要填上 ENTRY POINT(而不是你的帳戶),交易的接收者才是填入你的合約地址,交易內容會包含你身份的證明(例如簽章)、要送到代幣合約的相關資料等等,但不會附帶 Ether(因為 ENTRY POINT 本身也不會有錢),也不會有發送者對這筆交易的簽章(原本這種簽章是用來證明的確是發送者產生這筆交易的,因此交易如果不再附帶這種簽章則表示任何人都可以偽造其他人交易)。

礦工收到這筆交易後會先去試跑這筆交易做檢查,看你的合約會不會付手續費、看交易內容裡的資料有沒有通過你自己設的身份檢查和 nonce 值檢查,檢查都通過後就會去呼叫代幣合約發送代幣。

Account Abstraction 的問題

Account Abstraction 大大提升了使用者的自由度,但也大大提高了使用的門檻,使用者和開發者得要習慣全新一套運作的機制。另外因為任何人都可以偽造交易,礦工要先試跑每一筆交易來確認是否會收到手續費,這大大增加了礦工被無效交易 DoS 攻擊的可能性。
目前 ENTRY POINT 的功能被推延到未來的升級中。

CREATE2

留下來的 CREATE2 opcode 基本上和原本的 CREATE opcode 差異不大,目的都是用來產生新的合約,但不同的地方在於,藉由 CREATE2 你可以自由地控制合約產生的地址。CREATE2的 opcode 值為 0xF5

CREATE 計算新的合約該部署在哪個地址的時候會受到交易發起人(即 sender)和發起人當下的 nonce 值決定:

new_address = keccak256(rlp([sender, nonce]))[12:]

當然你也可以事先算出隨著 nonce 值一路增加,一路上會分別部署到哪些地址,但要能部署到你想要的地址的前提是 — 你必須先產生並執行這麼多筆交易(因為只有交易真的記錄在鏈上後你的 nonce 值才會增加)。

CREATE2 計算地址則是受到交易發起人、salt(32 byte 的任意值)和要部署的程式碼決定:

new_address =
keccak256( 0xff ++ sender ++ salt ++ keccak256(init_code)))[12:]
  • 0xff 是用來區分新舊地址產生的方式,舊的方式因為會先對 sendernonce 做 rlp 編碼,編碼完後最前面會是一個 0x01~0xff 之間的值,但 0xff 只有在被編碼的資料超級大才有可能出現,所以新的方式在最前面加了一個 0xff 來和舊的方式做區分。
  • init_code 是你要部署的合約的程式碼。
  • salt 是使用者可以任意指定的值,讓你可以把同一份合約部署在不同地址(如果沒有這個 salt 值,而且大家部署的init_code 又都一樣,則大家都會部署到相同的地址)。

有了 CREATE2 之後,你可以預先知道你的合約會部署到哪個地址且不用耗費太多成本,這對許多應用有不小的影響,像是 State Channel 或是用來躲避 Front Running 攻擊的 Submarine Send

測試鏈(Testnet)測試狀況

這次部署到 Ropsten 測試鏈進行硬分叉預演的時候出了點差錯而導致測試鏈出現多條分叉:

  • Parity 和 Geth 在計算 SSTORE 的 gas 收費上有不同(是 Parity 這邊的問題)
  • 原先的測試鏈使用者沒有更新版本
  • Parity 和 Geth 各自實作了不同的區塊回溯( revert)限制(可以視為他們自己加上的 Finality 條件),所以 Parity 的分叉鏈在距離分叉點超過一定數量的區塊後就再也沒辦法切換回正確的鏈了(除非刪掉原本的鏈的資料並重新同步一次)。

不過這次的插曲也凸顯了在測試鏈預演、同時有不同的客戶端軟體參加的重要性。Lane Rettig 在 Ethereum Magicians 的論壇上也整理了許多從這個事件所記取的教訓及能改進的地方。