函数选择器 selector

本章讲解在 Solidity 中,什么是函数选择器、函数签名,以及函数选择器的计算方法。

推特@Hita_DAO    DiscordHitaDAO

函数选择器(Function Selector)简称 selector,是一个 4 字节的哈希值,用于标识智能合约中的一个特定函数。

比如,0x7afbe4f1 就是下面 Add 函数的选择器:

function Add(uint256 a, uint256 b) public pure returns(uint256);

1. selector的计算方法

函数选择器是如何计算的呢?在回答这个问题之前,我们首先要弄懂一个概念:函数签名

函数签名(Function Signature)是使用文本字符来标识函数的一种方式。函数签名中包括函数名称和参数类型。

比如,上面 Add 函数的函数签名就是字符串 “Add(uint256,uint256)”

函数签名需要遵循的规则:

  1. 不包含返回值部分。
  2. 只包含参数类型,不包含参数名称。
  3. 参数间用逗号分隔,不能有空格。
  4. 不包含可见性和可变性等属性。

另外,参数类型必须使用规范写法,不能使用别名。比如,uintunit256 的别名,虽然两者实际是同一种类型,但在函数签名中,不能使用 uint,只能使用 unit256

我们弄懂了函数签名,也就容易理解函数选择器的计算方法了。

函数选择器是使用 keccak256 计算出函数签名的哈希值,然后取其前 4 个字节。

比如,计算 Add 的函数选择器的 Solidity 代码:

bytes4(keccak256(bytes("Add(uint256,uint256)")))

其中,最内层的 bytes("Add(uint256,uint256)")  将函数签名转为字节数组 bytes,再用 keccak256 对它求哈希值 ,最后取哈希值的前 4 个字节 bytes4

这是计算函数选择器的合约:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract CalcSelector {
  function selector() external pure returns (bytes4) {
    return bytes4(keccak256(bytes("Add(uint256,uint256)")));
  }
}

将合约部署到 Remix 上,调用 selector 函数,就可以得到 Add 函数的选择器为 0x7afbe4f1

2. selector的作用和原理

在以太坊上部署的二进制代码的合约中,使用函数选择器来定位和执行特定的函数,并传递相应的参数。

比如,一个合约中包含加法 Add 和减法 Sub 两个函数:

/ SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SelectorDemo {
  function Add(uint256 a, uint256 b) public pure returns(uint256){
    return a+b;
  }

  function Sub(uint256 a, uint256 b) public pure returns(uint256){
    return a+b;
  }
}

这个智能编译后,它的二进制代码的结构如下:

.............
.............
0x7afbe4f1: // Add函数入口
.............
.............
0x1ec9c2c0: // Sub函数入口
.............
.............

当外部调用合约的 Add 函数时,执行代码会提取请求数据中的函数选择器,然后开始匹配函数入口,当找到 0x7afbe4f1 时,发现两者相等,于是执行里面的代码。

3. msg.data

当外部合约或者客户端调用一个智能合约的函数,就需要向区块链提交一段二进制代码,在这段代码中就包含了被调用函数的选择器。

当被调用的合约函数收到请求数据,就会将请求数据,原封不动地封装在全局变量 msg.data 中。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MsgData {
  event Log(bytes data);

  function Add(uint256 a, uint256 b) public returns(uint256) {
    // 触发 Log 事件,输出 msg.data
    emit Log(msg.data);
    return a+b;
  }
}

将合约部署到 Remix 上,输入参数 1 和 2,然后调用 Add 函数:

 

调用 Add 函数后,会输出事件 Log ,它里面的 data 可以分为 3 个部分:

0x7afbe4f1 // Add 函数选择器
0000000000...000000001 // 参数 a 的值
0000000000...000000002 // 参数 b 的值

我们可以看到,请求数据的前 4 个字节,正是 Add 函数的选择器。