背 景
以太坊中的ecrecover函数可以用来获取对一条消息签名的地址。这对于证明一条消息或者一段数据被一个指定的账户签名过(而不是被篡改过)非常有用。但是 Qtum 没有使用以太坊的账户模型,而是采用比特币的 UTXO 模型,地址的算法也和以太坊不同,因此这个函数并不适用于 Qtum。在一些需要验证签名来源信息的情况下, Qtum 开发者并不能方便的在智能合约中完成这个验证,而是需要在合约中完整实现或者调用一次从签名和消息获取签名者公钥的合约,会造成非常大的开销,进而使得相应合约的调用费用非常高。
问题的细节
ecrecover接受一个消息的哈希和消息的签名,然后计算出签名的私钥对应的公钥,并将该公钥转换为以太坊地址格式。然而以太坊的地址算法和 Qtum 不同,而且ecrecover返回的是公钥经过哈希以后的结果,这个过程不可逆,因此在 Qtum 上无法使用这个函数。
在以太坊中,地址计算方法如下:
keccak256(pubkey)
而在 Qtum 上,地址的计算方式和比特币相同,使用如下计算方法:
ripemd160(sha256(pubkey))
在 Qtum 的合约中,msg.sender是一个 Qtum 地址。由于从公钥开始转换为地址的每一步操作都是不可逆的,ecrecover返回的以太坊地址无法和msg.sender中的 Qtum 地址进行比较。而现有的 Qtum 智能合约中并没有提供任何函数来从消息签名中获取 Qtum 地址,这导致 Qtum 智能合约开发者们不得不开发或使用Secp256k1相关的库来计算签名公钥和地址,造成更大的计算开销和更高的合约费用。
另一个需要注意的细节是,Qtum 沿用的比特币消息签名算法和以太坊的消息签名算法的实现上有一些细微的差别:
以太坊的签名按如下格式组成:
[r][s][v]
而 Qtum 的签名则是:
[v][r][s]
其中v是 recover id,r是椭圆曲线上的一个点R的X坐标,s是这个点R的Y坐标。如上的不同导致 Qtum 和以太坊的 recover 算法的实现细节也不相同。
QIP-6 的解决方案
通过在 Qtum 的虚拟机中增加一个预编译的合约,以提供一个用来调用 Qtum 核心代码中的 recover 代码的接口。智能合约开发者只需要写简单的一两个函数就能从签名消息中获取到签名者的地址。新增的预编译合约的接口和ecrecover保持一致。
什么是预编译合约
预编译合约是 EVM 中为了提供一些不适合写成 opcode 的较为复杂的库函数(多数用于加密、哈希等复杂计算)而采用的一种折中方案。由于它是用底层代码实现的,执行速度快,对于开发者来说就比直接用运行在 EVM 上的函数消耗更低。以太坊中使用预编译合约提供一些常用的较为繁琐的操作,比如sha256、ripemd160hash等。
预编译合约的实现
预编译合约的核心代码由虚拟机底层(C++)实现,通过在虚拟机的初始化过程中注册到人为指定的固定地址上来提供智能合约调用的接口。
预编译合约的使用
一个典型的调用方式:
assembly {
if iszero(call(gasLimit, contractAddress, value, input, inputLength, output, outputLength)) {
revert(0, 0)
}
}
在新版本的虚拟机中,还可以使用staticcall:
assembly {
success := staticcall(gasLimit, contractAddress, input, inputLength, output, outputLength)
}
其中contractAddress就是要调用的预编译合约的地址,本次 Qtum 新增的 btc_ecrecover 的地址是0x85。input是调用合约的参数列表。这个调用的返回值代表了调用是否成功,1表示成功,0表示失败。而返回的数据会写入到output里面。
QIP-6 的影响
QIP-6 大大减小了智能合约开发者的开发成本。从调用合约的角度来说,如果完全用 solidity 在合约中实现 recover,其 gas 使用量远远超过btc_ecrecover函数。于是使用btc_ecrecover来获取消息签名地址的合约调用成本也大大降低。此外,QIP-6 也让 Qtum 的智能合约系统更加完备。
另一方面,QIP-6 没有对原有的ecrecover进行修改,保持了 Qtum 和以太坊的兼容性,理论上不会带来任何风险。