一、什麼是函數選擇器與支持接口常量值
我們在瀏覽OpenZeppelin編寫的ERC721示例(模板)合約時,會看到這麼一段代碼:
/*
* bytes4(keccak256('balanceOf(address)')) == 0x70a08231
* bytes4(keccak256('ownerOf(uint256)')) == 0x6352211e
* bytes4(keccak256('approve(address,uint256)')) == 0x095ea7b3
* bytes4(keccak256('getApproved(uint256)')) == 0x081812fc
* bytes4(keccak256('setApprovalForAll(address,bool)')) == 0xa22cb465
* bytes4(keccak256('isApprovedForAll(address,address)')) == 0xe985e9c5
* bytes4(keccak256('transferFrom(address,address,uint256)')) == 0x23b872dd
* bytes4(keccak256('safeTransferFrom(address,address,uint256)')) == 0x42842e0e
* bytes4(keccak256('safeTransferFrom(address,address,uint256,bytes)')) == 0xb88d4fde
*
* => 0x70a08231 ^ 0x6352211e ^ 0x095ea7b3 ^ 0x081812fc ^
* 0xa22cb465 ^ 0xe985e9c ^ 0x23b872dd ^ 0x42842e0e ^ 0xb88d4fde == 0x80ac58cd
*/
bytes4 private constant _INTERFACE_ID_ERC721 = 0x80ac58cd;
constructor () public {
// register the supported interfaces to conform to ERC721 via ERC165
_registerInterface(_INTERFACE_ID_ERC721);
}
有沒有讀者和我一樣好奇它的含義是什麼呢?它代表一個標準的ERC721智能合約應該支持(實現)的接口,分別爲:'balanceOf(address)'
、'ownerOf(uint256)')
…'safeTransferFrom(address,address,uint256,bytes)')
。
這其中balanceOf(address)
叫着函數的signature。學過函數重載的讀者都知道,函數重載是根據函數名稱和參數列表區分的,並不包括返回參數。所以這裏的signature只有函數名稱和參數類型列表,參數之間用逗號區分,並且不包含多餘空格。
使用bytes4(keccak256('balanceOf(address)'))
方法計算出來的值叫着函數選擇器,它是智能合約調用數據的最開頭四個字節。智能合約根據這個選擇器來確定調用的是哪一個函數,選擇器的計算方法在註釋中已經列出。
標準的ERC721智能合約必須支持以上9個接口,但是不能逐個驗證(太低效了)。所以將9個函數選擇器相異或(注意接口異或的順序並不影響結果),得到一個bytes4
類型的常量值來代表該系列接口,最後在構造器裏註冊這個常量值就OK了。
溫馨提示:
在上面的註釋中,0xa22cb465 ^ 0xe985e9c ^ 0x23b872dd ^ 0x42842e0e ^ 0xb88d4fde ==
這一行中0xe985e9c
少了一個數字5,這應該屬於OpenZeppelin的一個筆誤,這裏爲了保持原樣就未修改它。希望讀者驗證註釋時能夠注意到這一點,不要也少一個5,這樣就得不到正確的結果。
二、在什麼情況下需要我們手動計算函數選擇器和支持接口常量值
通常情況下,我們不需要計算函數選擇器或者支持接口值。但是當你想增加一個(系列)接口而又想合約能夠表明支持或者不支持這個(系列)接口時,你就需要手動計算你的函數選擇器和支持接口常量值。注意:當只有一個接口(函數)時,支持接口常量值就是該函數選擇器。我們舉一個實際應用的例子。
Alpha Wallet (https://alphawallet.com/) 在顯示ERC721代幣時,爲了一次性獲取用戶所有的代幣ID(標準ERC721不提供這個接口,見第一節的註釋),自己增加了一個getBalances
方法:
function getBalances(address owner) public view returns(uint256[] memory) {
return balances[owner];
}
因此,它計算了該函數的選擇器作爲支持常量值(只增加了一個函數,所以支持常量值就是該函數選擇器,多個函數纔是選擇器相異或)。
/* bytes4(keccak256('getBalances(address)')) == 0xc84aae17 */
bytes4 private constant _INTERFACE_ID_HONOR_BALANCES = 0xc84aae17;
constructor (string memory name, string memory symbol) ERC721Metadata(name,symbol) public {
_registerInterface(_INTERFACE_ID_HONOR_BALANCES);
}
其中_INTERFACE_ID_HONOR_BALANCES
這個常量名稱是自定義的,但是值是根據bytes4(keccak256('getBalances(address)'))
計算出來的。下面我們分別使用Solidity和Python進行計算實現,方法很簡單。
三、使用Solidity計算
Solidity不同於其它編程語言,它不是解釋後執行或者編譯後執行,只能寫成智能合約部署在以太坊上供大家調用時執行,所以我們需要編寫一個簡單的智能合約,代碼如下:
pragma solidity ^ 0.5 .0;
contract CalSelector {
/**
* 給定一個函數signature,如 'getSvg(uint256)',計算出它的選擇器,也就是調用數據最開始的4個字節
* 該選擇器同時也可用於標明合約支持的接口,如alpha錢包對ERC721標準增加的getBalances接口
* bytes4(keccak256('getBalances(address)')) == 0xc84aae17
*/
function getSelector(string memory signature) public pure returns(bytes4) {
return bytes4(keccak256(bytes(signature)));
}
/**
* 用來計算合約支持的一系列接口的常量值,計算方法是將所有支持接口的選擇器相異或
* 例如 ERC721元數據擴展接口
* bytes4(keccak256('name()')) == 0x06fdde03
* bytes4(keccak256('symbol()')) == 0x95d89b41
* bytes4(keccak256('tokenURI(uint256)')) == 0xc87b56dd
*
* => 0x06fdde03 ^ 0x95d89b41 ^ 0xc87b56dd == 0x5b5e139f
*/
function getSupportedInterface(bytes4[] memory selectors) public pure returns(bytes4) {
bytes4 result = 0x00000000;
for (uint i = 0; i < selectors.length; i++) {
result = result ^ selectors[i];
}
return result;
}
}
合約很簡單,就兩個函數,第一個函數照搬註釋中的方法計算函數選擇器。注意,爲了方便,輸入參數的類型爲字符串,計算前先要轉換成bytes4
類型。第二個函數輸入參數爲所有的函數選擇器,這裏的bytes4 result = 0x00000000;
這一句代碼是因爲0異或任何值爲該值本身,而4字節剛好是8位16進制。注意這兩個函數返回的值都是bytes4
類型。
合約部署流程本文就不介紹了,先跳過,下面先簡要介紹合約調用。
四、使用Python連接以太坊上的智能合約
使用Python連接以太坊的智能合約,需要使用web3.py
這個庫。首先安裝它:
$ pip install web3
我們通過infura
節點來連接以太坊,所以你還需要一個INFURA_PROJECT_ID
。然後將它設置進環境變量:
$ export WEB3_INFURA_PROJECT_ID=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
不過你沒有這個INFURA_PROJECT_ID
也沒關係,經過測試發現,暫時不設置該環境變量也可以連接到智能合約。如果爲了保險或者發現後面的代碼運行不正確,還是需要去infura網站新建一個工程然後獲取你自己的project_id。
我們先使用python構建一個連接以太坊智能合約的對象,在工作目錄下新建contract.py
,代碼如下:
# 連接一個Kovan測試網上的用來計算支持接口常量值的智能合約
from web3.auto.infura.kovan import w3
# 合約ABI。注意,這裏所有的true和false要替換成python的True和False
contract_abi = [
{
"constant": True,
"inputs": [
{
"internalType": "string",
"name": "signature",
"type": "string"
}
],
"name": "getSelector",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"payable": False,
"stateMutability": "pure",
"type": "function"
},
{
"constant": True,
"inputs": [
{
"internalType": "bytes4[]",
"name": "selectors",
"type": "bytes4[]"
}
],
"name": "getSupportedInterface",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"payable": False,
"stateMutability": "pure",
"type": "function"
}
]
# Kovan測試網上合約地址
contract_address = '0x07d74Cf0Ce4A1b10Ece066725DB1731515d62b76'
# 構造合約對象
CalSelector = w3.eth.contract(address=contract_address,abi=contract_abi)
構建一個合約對象需要合約的ABI和地址,基於免費的原則,我們把本文第三節的CalSelector
智能合約部署在kovan
測試網上。
需要注意的是,由於該合約簡單,ABI較小,所以直接寫在了代碼中,而通常合約ABI位於一個獨立的文件中。由於合約編譯(使用truffle編譯)後的ABI中true
和false
是首字母小寫的,而python又與衆不同的搞了一個首字母大寫,所以需要手動替換成True
和False
。如果將合約ABI保存在獨立文件中,讀取該文件後先需要使用json
模塊的loads
方法轉成字典,再獲取的['abi']
屬性,此時不需要手動替換true
和false
。
五、使用python計算並且兩者對照
在同一工作目錄下新建test.py
,代碼如下:
from web3.auto.infura.kovan import w3
from contract import CalSelector
# bytes4(keccak256('isApprovedForAll(address,address)')) == 0xe985e9c5
func = 'isApprovedForAll(address,address)'
# 0x70a08231 ^ 0x6352211e ^ 0x095ea7b3 ^ 0x081812fc ^
# 0xa22cb465 ^ 0xe985e9c ^ 0x23b872dd ^ 0x42842e0e ^ 0xb88d4fde == 0x80ac58cd
# 注意,接口出現的順序並不影響計算結果,這個也是顯然亦見的
selectors = [
0x70a08231,
0x6352211e,
0x095ea7b3,
0x081812fc,
0xa22cb465,
0xe985e9c5,
0x23b872dd,
0x42842e0e,
0xb88d4fde
]
def calSelectorByPython(_func):
result = w3.keccak(text=_func)
selector = (w3.toHex(result))[:10]
return selector
def calSelectorBySolidity(_func):
selector = CalSelector.functions.getSelector(_func).call()
return w3.toHex(selector)
def calSupportedInterfaceByPython(_selectors):
result = int('0x00000000',16)
for selector in _selectors:
result = result ^ selector
return w3.toHex(result)
def calSupportedInterfaceBySolidity(_selectors):
_param = [ w3.toBytes(selector) for selector in _selectors]
supported_interface = CalSelector.functions.getSupportedInterface(_param).call()
return w3.toHex(supported_interface)
if __name__ == "__main__":
print(calSelectorByPython(func))
print(calSelectorBySolidity(func))
print('-------------------------')
print(calSupportedInterfaceByPython(selectors))
print(calSupportedInterfaceBySolidity(selectors))
代碼也很簡單,定義了四個函數,分別是使用python和使用合約計算函數選擇器、使用python和使用合約來計算支持接口常量值。
代碼裏直接使用本文第一節的註釋來進行驗證,注意我提到過的那個筆誤。直接運行test.py
:
➜ python python3 test.py
0xe985e9c5
0xe985e9c5
-------------------------
0x80ac58cd
0x80ac58cd
從輸出中可以看出,使用python計算的結果和使用合約計算的結果是一致的,並且和註釋中的數字是相同的。需要說明的一點是函數選擇器和接口常量值都是bytes4
類型的,在python中是bytes
類型,爲了方便顯示,我們將它們轉換成了16進制字符串形式。
六、直接在etherscan上計算
從前面的內容可以看到,連接以太坊智能合約需要一系列操作,還是有一點點麻煩的。如果我們不想使用python或者其它的編程語言(例如JavaScript)來連接以太坊智能合約進行計算,又該怎麼辦呢?不用擔心,我們還有一種直接在web頁面上調用合約的方法。
我們知道,如果一個智能合約開源,那麼可以在etherscan上直接調用該合約的方法。利用這一點我們可以直接在etherscan上計算函數選擇器和支持接口常量值。
訪問如下網址,URL中最後的地址就是本文第三節的合約在kovan測試網上的地址:
https://kovan.etherscan.io/address/0x07d74cf0ce4a1b10ece066725db1731515d62b76#readContract
溫馨提示:
由於無法直接訪問etherscan,所以需要科學上網。
打開網頁後點擊下方的Contract(它旁邊有一個綠色的勾代表它已經開源過),如果點擊Code就會顯示合約源代碼和ABI。我們點擊Read Contract,下面的列表裏就會出現本文合約中定義的那兩個函數。
注意:未通過開源認證的合約不能直接調用對應的函數。
在計算函數選擇器參數裏輸入getBalances(address)
(這個是Alpha錢包對ERC721增加的自定義接口),點擊Query,查詢結束後就會得到相應的結果。同樣, 我們在getSupportedInterface
方法裏輸入[0x06fdde03,0x95d89b41,0xc87b56dd]
(這個是ERC721元數據擴展接口的所有函數選擇器),也會得到相應的結果。
大家也可以收藏這個網址,以便有需要時可以直接訪問來進行相關計算。
好了,以上就是本文的全部內容,不足或者錯誤之處歡迎大家留言指正。