# EIP-7069 改良版呼叫指令
# 注意事項
本文並沒有經過作者外的其他審查者審核,因此若內容有誤,請到 issue 區提出問題,我會儘速修改,謝謝。
# 摘要
由於 EOFv1 導入新的合約結構,將資料區段與程式碼區段徹底分離,因此現有的 EVM *Call
系列指令已經無法很合適地使用。
為了避免指令語意的歧義性,並簡化呼叫的行為。本 EIP 除了將原先的 *Call
指令全部棄用外,也在程式碼驗證中完全拒絕。
然而呼叫指令的需求依然存在,作為替代方案,本 EIP 將定義三個新的呼叫指令:EXTCALL
、EXTDELEGATECALL
和 EXTSTATICCALL
,它們具有更簡單的運作方式。同時也引入另一個回傳資料相關的指令 RETURNDATALOAD
,用於將回傳資料載入堆疊中。
此外,在 EOFv1 格式的程式碼中,RETURNDATACOPY
指令的行為也會被修改。。
另一個值得一題的新限制是新指令將不再允許指定燃料(gas)限制,而是依靠「63/64規則」(來自EIP-150)來限制燃料。這將導致「津貼」相關的規則被簡化,呼叫者不再需要透過特殊計算來判斷是否津貼是否充足。
此外,指定輸出緩衝區位址的舊功能被移除,改為使用 RETURNDATACOPY
。對於之前會使用 *CALL
將輸出放入緩衝區然後用 MLOAD
從緩衝區讀取的情況,現在將透過直接使用 RETURNDATALOAD
來替代。
最後,新指令不再回傳布林值來表示執行狀態,而是回傳一個可擴展的狀態碼列表:0
表示成功,1
表示回復(REVERT),2
表示失敗(FAILURE)。
# 動機
燃料的可觀察性長期以來一直是個問題。由於不同的 EIP 可能涉及 Gas 燃料價格與計價方式的變動,在呼叫指令中能夠顯式指定 Gas 限制的設計從彈性的優勢逐漸轉變成每次硬分岔後都有可能發生意外的未爆彈。在本 EIP 中移除了新指令中的燃料可選擇性,這樣能使的未來所有智慧合約不再受到燃料重新定價影響。此外,在 EOFv1 合約中也禁止與棄用舊的呼叫指令,確保這些合約也不受過往的燃料費用變化影響。
雖然有不少對移除燃料的可觀察性的反對者,但 Solidity 0.4.21 開始,除非開發者在語法中使用明確的覆蓋({gas: ...}
),否則編譯器預設就會將所有剩餘的燃料傳遞給呼叫(使用 call(gas(), ...
),跟本 EIP 的新呼叫指令邏輯一致。且實務上除非對於成本控制有極大需求,否則大多數合約也不依賴與使用控制燃料的語法特性。
除了上述變更外,本 EIP 還導入了一個便利的功能:回傳更詳細的狀態碼:成功(0)
,回復(REVERT)(1)
,失敗(FAILURE)(2)
。這將布林選項改為狀態碼,未來可以擴展。
最後,RETURNDATA*
指令(EIP-211)的引入已經使原本呼叫的輸出參數變得過時,在大多數情況下它們未被使用,而是直接對 RETURN DATA 進行操作。使用輸出緩衝區過去曾造成「錯誤」:在 ERC-20 的案例中,衝突的實作造成了很多麻煩,回傳或者不回傳資料的實作都有人撰寫且部署,導致了行為的不一致。通過依賴 RETURNDATA*
指令將徹底解決這個問題。且本 EIP 也加入了「缺失的」RETURNDATALOAD
指令來完善回傳資料區段的存取指令。
# 規格
以下說明中若有特殊常數,將在下方的表格中列出,請注意這些參數可能隨著新的 EIP 或者硬分岔的實裝而變動:
| 名稱 | 值 | 註解 |
|-----------------------|-------|------------------------|
| WARM_STORAGE_READ_COST| 100 | 來自 EIP-2929 |
| COLD_ACCOUNT_ACCESS | 2600 | 來自 EIP-2929 |
| CALL_VALUE_COST | 9000 | |
| ACCOUNT_CREATION_COST | 25000 | |
| MIN_RETAINED_GAS | 5000 | |
| MIN_CALLEE_GAS | 2300 | |
本 EIP 提供的四個新指令:
EXTCALL
(0xf8
) 參數為(目標地址, 輸入偏移量, 輸入大小, 值)
EXTDELEGATECALL
(0xf9
) 參數為(目標地址, 輸入偏移量, 輸入大小)
EXTSTATICCALL
(0xfb
) 參數為(目標地址, 輸入偏移量, 輸入大小)
RETURNDATALOAD
(0xf7
) 參數為偏移量
這四個新指令在傳統程式碼中未定義,僅在 EOFv1 程式碼中可用。
EXT*CALL
的執行語意:
- 收取
WARM_STORAGE_READ_COST
燃料。 - 從堆疊中彈出所需參數,若堆疊下溢則以異常失敗終止。
- 注意:當在 EOFv1 中實作時,堆疊下溢檢查是在堆疊驗證期間完成的,執行時檢查會被省略。
- 如果
值
不為零,以為著有 ether 的轉移(僅EXTCALL
):- 如果當前處於
static-mode
,則以異常失敗終止。 - 收取
CALL_VALUE_COST
燃料。
- 如果當前處於
- 如果
目標地址
不合法(即在 20 位元組的地址規格外還有其他多餘的資料),則以異常失敗終止。 - 使用
[輸入偏移量, 輸入大小]
執行(並收費)記憶體擴展。 - 如果
目標地址
不在warm_account_list
中,收取COLD_ACCOUNT_ACCESS - WARM_STORAGE_READ_COST
燃料。 - 如果
目標地址
不在世界狀態中,且呼叫設定會導致帳戶創建,收取ACCOUNT_CREATION_COST
燃料。- 這個 EIP 中唯一的情況是
值
不為零(僅EXTCALL
)。
- 這個 EIP 中唯一的情況是
- 計算可用給被呼叫者的燃料為「呼叫者剩餘燃料」減去 「
max(floor(gas/64), MIN_RETAINED_GAS)
」。 - 清除回傳資料緩衝區段。
- 如果以下任何一項為真,則以狀態碼
1
回傳到堆疊上失敗(僅消耗到這一點的燃料):- 此時被呼叫者可用的燃料少於
MIN_CALLEE_GAS
。 - 當前帳戶的餘額小於
值
(僅EXTCALL
)。 - 當前呼叫堆疊深度等於
1024
。 - (僅
EXTDELEGATECALL
)狀態中的目標地址
帳戶沒有可執行的 EOFv1 程式碼
- 此時被呼叫者可用的燃料少於
- 使用可用的燃料和設定執行呼叫。
- 在堆疊上放置一個狀態碼:
- 如果呼叫成功,則為
0
。 - 如果呼叫已回復,則為
1
(如果在步驟 10 已經失敗了,會更早,也會馬上放置狀態碼後提前回傳)。 - 如果呼叫已失敗,則為
2
。
- 如果呼叫成功,則為
- 被呼叫者未使用的燃料會返回給呼叫者。
RETURNDATALOAD
的執行語意:
- 收取
3
燃料 - 從堆疊中彈出1個項目,稱為
偏移量
- 將1個項目推送到堆疊上,這是從
偏移量
開始的回傳資料緩衝區中讀取的32位元組。 - 如果
偏移量 + 32 > 回傳資料緩衝區長度
,結果將用零填充。
在 EOFv1 格式的程式碼(EIP-3540)中,RETURNDATACOPY
的執行語意修改如下:
- 假設從堆疊中彈出的3個參數是
destOffset
、offset
和size
。(沒有變化) - 執行記憶體擴展到
destOffset + size
並扣除記憶體擴展成本。(沒有變化) - 如果
offset + size > 回傳資料緩衝區長度
,不要以異常失敗終止,而是將複製後的記憶體位元組後的offset + size - 回傳資料緩衝區長度
個記憶體位元組設為零。 - 記憶體複製的燃料費用仍為
3 * num_words(size)
,不管實際複製或設為零的位元組數量。
不在 EOFv1 格式程式碼中的 RETURNDATACOPY
執行(即在傳統程式碼中)不會改變。
# 新舊指令比較
+------------------+ +------------------+
| 舊式CALL指令 | | 新式EXTCALL指令 |
+------------------+ +------------------+
| - 可以控制燃料 | | - 自動計算燃料 |
| - 需手動計算津貼 | | - 固定津貼機制 |
| - 回傳布林值 | | - 回傳狀態碼(0,1,2)|
| - 可指定輸出緩衝區 | | - 使用RETURNDATA* |
+------------------+ +------------------+
# 原理
# 移除燃料可選擇性
與舊有的 *CALL
系列指令的主要差異在於呼叫者無法控制作為呼叫時傳遞的燃料量,在過去,有些合約會在呼叫時設定燃料上限,以永久限制後續呼叫鏈中的花費燃料總數。而在新的指令終將會把全部剩餘燃料都傳遞過去。雖然移除燃料可選擇性會導致現在建構於此特性之上的分析失去作用,但同時還帶來數個有價值的好處:
- 不論未來的 EIP 與硬分岔對於燃料定價怎麼修改,將不會破壞因為呼叫時燃料限制導致的失敗
- 由於此特性,如果發現在交易時發生燃料不足的問題(Out of Gas),將可以透過在發送交易時直接增加 Gas 的數量來解決。過去這個 OOG 發生的原因除了交易時給的數量不夠外,也可能是合約開發者所限制的呼叫時燃料數量導致的。
# 津貼與63/64規則
津貼的目的是在呼叫「合約錢包」時有足夠的燃料來發出日誌(Log)。只有在使用 CALL
指令且值不為零時才會添加津貼。
63/64規則有多個目的:
- 限制呼叫深度
- 確保呼叫者在被呼叫者返回後有燃料剩餘以進行狀態更改。
此外,還有一個呼叫深度計數器,如果深度超過1024,呼叫將失敗。
在引入63/64規則之前,需要在呼叫者方面相對精確地計算可用燃料。Solidity有一個複雜的規則集,它試圖估計在呼叫者方面執行呼叫本身將花費多少,以便設定一個合理的燃料值。
雖然在本 EIP 中改變了呼叫指令的規則,但63/64規則仍然適用:
- 執行被呼叫者之前至少保留
MIN_RETAINED_GAS
燃料, - 被呼叫者至少有
MIN_CALLEE_GAS
燃料可用。
MIN_CALLEE_GAS
規則是津貼的替代:它簡化了對燃料成本的推理,並且統一應用於所有引入的 EXT*CALL
指令。
下表顯示了差異(注意 CALL
的_呼叫者所需燃料_和_呼叫者成本_之間的差異)。
+-------------------+--------------------+-------------------+--------------------+---------------+
| | 呼叫者所需燃料 | 呼叫者成本 | 呼叫者最小保留燃料 | 被呼叫者最小燃料 |
+-------------------+--------------------+-------------------+--------------------+---------------+
| CALL V=0 | 100 | 100 | 0 | 0 |
| CALL V≠0 | 100+9000 | 100+6700 | 0 | 2300 |
| DELEGATECALL | 100 | 100 | 0 | 0 |
| STATICCALL | 100 | 100 | 0 | 0 |
| EXTCALL V=0 | 100 | 100 | 5000 | 2300 |
| EXTCALL V≠0 | 100+9000 | 100+9000 | 5000 | 2300 |
| EXTDELEGATECALL | 100 | 100 | 5000 | 2300 |
| EXTSTATICCALL | 100 | 100 | 5000 | 2300 |
+-------------------+--------------------+-------------------+--------------------+---------------+
- 呼叫者所需燃料:呼叫者執行呼叫指令所需的最小燃料量,較低的值會導致呼叫者OOG,
- 呼叫者成本(燒掉的燃料):從呼叫者扣除的燃料量來執行指令,這個量對被呼叫者不可用,
- 呼叫者最小保留燃料:呼叫者在呼叫後保證擁有的最小燃料量,如果不能保證,呼叫在到達被呼叫者之前就會失敗,
- 被呼叫者最小燃料:被呼叫者執行的最小燃料限制。
# 輸出緩衝區
移除了指定輸出緩衝區地址的功能,因為它增加了複雜性,而且在大多數情況下,實作者更喜歡使用 RETURNDATACOPY
。即使他們依賴輸出緩衝區(如Vyper的情況),他們仍然會用 RETURNDATASIZE
檢查長度。在Solidity中,一個例外是預期的回傳大小已知的情況(即非動態回傳值),在這種情況下,Solidity仍然使用輸出緩衝區。對於這些情況,引入了 RETURNDATALOAD
,它簡化了將回傳資料複製到(已知的)輸出緩衝區並從那裡使用 MLOAD
的工作流程;相反,可以直接使用 RETURNDATALOAD
。
# 狀態碼
當前的 *Call
呼叫指令回傳一個布林值來表示成功:0表示失敗,1表示成功。
Solidity 編譯器假設這個值是一個布林值,因此使用這個值作為狀態的分支條件(if iszero(status) { /* 失敗 */ }
)。
但也因為這個設計,導致 EVM 無法在不破壞現有合約的情況下引入新的狀態碼。
為了徹底解決這個問題,在本 EIP 中將型態從布林值更改為非負整數的狀態碼,其中 0
表示成功,因此將來可以更輕鬆地加入更多的非成功的狀態碼。(但以目前 Ethereum 的生態來說,想新增任何狀態碼的討論都是曠日費時的,畢竟連 EOFv1 這麼棒的特性都被雪藏了好幾年了呢)
狀態碼 1
用於來自被呼叫時的回復和在指令執行中遇到的輕度失敗(見執行語意的步驟10)。之所以把這兩個錯誤合併的原因除了要保持與原始 *CALL
類似的語意(及兩種情況都保留未使用的燃料),並且繼續對呼叫者來說是不可區分的。
狀態碼 2
表示被呼叫執行中的異常失敗,意味著發生了錯誤,消耗了被呼叫時的所有剩餘燃料。
# 參數順序
為了對其全部的指令參數,統一將 值
欄位移到最後。這使得所有的 EXT*CALL
具備同樣的參數順序,只有最後一個參數例外,可以稍微簡化了 EVM 和編譯器實作細節。
# 新舊指令流程比較
舊式CALL流程: 新式EXTCALL流程:
+------------------+ +------------------+
| 開始 | | 開始 |
+------------------+ +------------------+
| |
v v
+------------------+ +------------------+
| 指定目標地址 | | 指定目標地址 |
+------------------+ +------------------+
| |
v v
+------------------+ +------------------+
| 手動計算並設定燃料 | | 自動計算燃料 |
+------------------+ +------------------+
| |
v v
+------------------+ +------------------+
| 設定輸出緩衝區 | | 執行呼叫 |
+------------------+ +------------------+
| |
v v
+------------------+ +------------------+
| 執行呼叫 | | 檢查回傳狀態碼 |
+------------------+ | (0:成功,1:恢復,2:失敗)|
| +------------------+
v |
+------------------+ v
| 檢查回傳布林值 | +------------------+
| (0:失敗,1:成功) | | 使用RETURNDATA* |
+------------------+ | 存取回傳資料 |
+------------------+