我们在浏览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不同于其它编程语言,它不是解释后执行或者编译后执行,只能写成智能合约部署在以太坊上供大家调用时执行,所以我们需要编写一个简单的智能合约,代码如下:
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连接以太坊的智能合约,需要使用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
。
在同一工作目录下新建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进制字符串形式。
从前面的内容可以看到,连接以太坊智能合约需要一系列操作,还是有一点点麻烦的。如果我们不想使用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元数据扩展接口的所有函数选择器),也会得到相应的结果。
大家也可以收藏这个网址,以便有需要时可以直接访问来进行相关计算。
好了,以上就是本文的全部内容,不足或者错误之处欢迎大家留言指正。