使用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元数据扩展接口的所有函数选择器),也会得到相应的结果。

       大家也可以收藏这个网址,以便有需要时可以直接访问来进行相关计算。

       好了,以上就是本文的全部内容,不足或者错误之处欢迎大家留言指正。

你可能感兴趣的:(以太坊)