使用python和solidity分別計算以太坊智能合約函數選擇器和支持接口常量值

一、什麼是函數選擇器與支持接口常量值

       我們在瀏覽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中truefalse是首字母小寫的,而python又與衆不同的搞了一個首字母大寫,所以需要手動替換成TrueFalse。如果將合約ABI保存在獨立文件中,讀取該文件後先需要使用json模塊的loads方法轉成字典,再獲取的['abi']屬性,此時不需要手動替換truefalse

五、使用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元數據擴展接口的所有函數選擇器),也會得到相應的結果。

       大家也可以收藏這個網址,以便有需要時可以直接訪問來進行相關計算。

       好了,以上就是本文的全部內容,不足或者錯誤之處歡迎大家留言指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章