使⽤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,这样就得不到正确的结果。
python怎么读取py文件⼆、在什么情况下需要我们⼿动计算函数选择器和⽀持接⼝常量值
通常情况下,我们不需要计算函数选择器或者⽀持接⼝值。但是当你想增加⼀个(系列)接⼝⽽⼜想合约能够表明⽀持或者不⽀持这个(系列)接⼝时,你就需要⼿动计算你的函数选择器和⽀持接⼝常量值。注意:当只有⼀个接⼝(函数)时,⽀持接⼝常量值就是该函数选择器。我们举⼀个实际应⽤的例⼦。
Alpha Wallet () 在显⽰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也没关系,经过测试发现,暂时不设置该环境变量也可以连接到智能合约。如果为了保险或者发现后⾯的代码运⾏不正确,还是需要去新建⼀个⼯程然后获取你⾃⼰的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 = act(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,代码如下:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论