背 景
以太坊中的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里面。
下面我们看一个例子:
pragma solidity ^0.5.0;
/**
* @title Elliptic curve signature operations
* @dev Based on
https://gist.github.com/axic/5b33912c6f61ae6fd96d6c4a47afde6d
* TODO Remove this library once solidity supports passing a signature to ecrecover.
* See https://github.com/ethereum/solidity/issues/864
*/
library ECDSA {
/**
* @dev Recover signer address from a message by using their signature.
* @param hash bytes32 message, the hash is the signed message. What is recovered is the signer address.
* @param signature bytes signature, the signature is generated using web3.eth.sign()
*/
function recover(bytes32 hash, bytes memory signature) internal view returns (address) {
// Check the signature length
if (signature.length != 65) {
return (address(0));
}
// Divide the signature in r, s and v variables
bytes32 r;
bytes32 s;
uint8 v;
// ecrecover takes the signature parameters, and the only way to get them
// currently is to use assembly.
// solhint-disable-next-line no-inline-assembly
assembly {
v := byte(0, mload(add(signature, 0x20)))
r := mload(add(signature, 0x21))
s := mload(add(signature, 0x41))
}
// EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
// unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
// the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most
// signatures from current libraries generate a unique signature with an s-value in the lower half order.
//
// If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
// with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 – s1 and flip v from 27 to 28 or
// vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
// these malleable signatures as well.
if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
return address(0);
}
// Support both compressed or uncompressed
if (v != 27 && v != 28 && v != 31 && v != 32) {
return address(0);
}
// If the signature is valid (and not malleable), return the signer address
return btc_ecrecover(hash, v, r, s);
}
function btc_ecrecover(bytes32 msgh, uint8 v, bytes32 r, bytes32 s) public view returns(address) {
uint256[4] memory input;
input[0] = uint256(msgh);
input[1] = v;
input[2] = uint256(r);
input[3] = uint256(s);
uint256[1] memory retval;
uint256 success;
assembly {
success := staticcall(not(0), 0x85, input, 0x80, retval, 32)
}
if (success != 1) {
return address(0);
}
return address(retval[0]);
}
}
在上面这个例子中,只要调用btc_ecrecover函数就能获取到消息签名者的地址。为了简化输入,例子中也封装了一个recover函数,使得开发者只要传入原始签名就能完成合约调用。
下面我们不使用预编译合约,而是完全使用 solidity 代码实现 btc_ecrecover功能。代码实现如下:
pragma solidity ^0.4.26;
import {ECCMath} from “github.com/androlo/standard-contracts/contracts/src/crypto/ECCMath.sol”;
import {Secp256k1} from “github.com/androlo/standard-contracts/contracts/src/crypto/Secp256k1.sol”;
library ECDSA {
uint256 constant p = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f;
uint256 constant n = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141;
uint256 constant gx = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798;
uint256 constant gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8;
function recover(bytes32 hash, bytes memory signature) internal view returns (address) {
if (signature.length != 65) {
return (address(0));
}
bytes32 r;
bytes32 s;
uint8 v;
assembly {
v := byte(0, mload(add(signature, 0x20)))
r := mload(add(signature, 0x21))
s := mload(add(signature, 0x41))
}
if (uint256(s) > n / 2) {
return address(0);
}
if (v != 27 && v != 28 && v != 31 && v != 32) {
return address(0);
}
return btc_ecrecover(hash, v, r, s);
}
function btc_ecrecover(bytes32 msgh, uint8 v, bytes32 r, bytes32 s) public view returns (address) {
uint i = 0;
uint256 rr = uint256(r);
uint256 ss = uint256(s);
bool isYOdd = ((v – 27) & 1) != 0;
bool isSecondKey = ((v – 27) & 2) != 0;
bool isCompressed = ((v – 27) & 4) != 0;
if (rr >= p % n && isSecondKey) {
return address(0);
}
uint256[3] memory P = _getPoint(uint256(msgh), rr, ss, isYOdd, isSecondKey);
if (P[2] == 0) {
return address(0);
}
ECCMath.toZ1(P, p);
bytes memory publicKey;
if (isCompressed) {
publicKey = new bytes(33);
publicKey[0] = byte(P[1] % 2 == 0 ? 2 : 3);
for (i = 0; i < 32; ++i) {
publicKey[32 – i] = byte((P[0] >> (8 * i)) & 0xff);
}
} else {
publicKey = new bytes(65);
publicKey[0] = 4;
for (i = 0; i < 32; ++i) {
publicKey[32 – i] = byte((P[0] >> (8 * i)) & 0xff);
publicKey[64 – i] = byte((P[1] >> (8 * i)) & 0xff);
}
}
return address(ripemd160(sha256(publicKey)));
}
function _getPoint(uint256 msgh, uint256 r, uint256 s, bool isYOdd, bool isSecondKey) internal view returns (uint256[3] memory) {
uint256 rx = isSecondKey ? r + n : r;
uint256 ry = ECCMath.expmod(ECCMath.expmod(rx, 3, p) + 7, p / 4 + 1, p);
if (isYOdd != (ry % 2 == 1)) {
ry = p – ry;
}
uint256 invR = ECCMath.invmod(r, n);
return Secp256k1._add(
Secp256k1._mul(n – mulmod(msgh, invR, n), [gx, gy]),
Secp256k1._mul(mulmod(s, invR, n), [rx, ry])
);
}
}
我们在测试链上部署了上述两个两个合约,地址分别如下:
预编译合约: 21ea1d8376d1820d7091084a76f380143b59aaf8
solidity实现: 4fdff1b4bde5edf13360ff0946518a01115ce818
使用地址
qQqip6i2e2buCZZNdqMw4VNpaYpnLm4JAx对消息btc_ecrecover test进行签名,我们得到btc_ecrecover 的调用参数:
bytes32 msgh = 0xdfa80e3294fd8806ab908904403db376b3dd35c6356ab2d3b884db4f6ec5e93d
uint8 v = 0x20
bytes32 r = 0xca08c0813407de3a78053c976462eacbde3fd69843e21acf8dd636149bf4b753
bytes32 s = 0x0731bce3ed9b489da0165af79759c1d586ef8fe53b3aab95fcab68d01ed6f156
两个合约调用调用结果如下:
1.预编译合约
callcontract 21ea1d8376d1820d7091084a76f380143b59aaf8 69bc0963dfa80e3294fd8806ab908904403db376b3dd35c6356ab2d3b884db4f6ec5e93d0000000000000000000000000000000000000000000000000000000000000020ca08c0813407de3a78053c976462eacbde3fd69843e21acf8dd636149bf4b7530731bce3ed9b489da0165af79759c1d586ef8fe53b3aab95fcab68d01ed6f156
{
“address”: “21ea1d8376d1820d7091084a76f380143b59aaf8”,
“executionResult”: {
“gasUsed”: 32688,
“excepted”: “None”,
“newAddress”: “21ea1d8376d1820d7091084a76f380143b59aaf8”,
“output”: “0000000000000000000000004fdff1b4bde5edf13360ff0946518a01115ce818”,
“codeDeposit”: 0,
“gasRefunded”: 0,
“depositSize”: 0,
“gasForDeposit”: 0,
“exceptedMessage”: “”
},
“transactionReceipt”: {
“stateRoot”: “5d9e1ad1b5d09e9e7c41d09078434488927366adf8ebf5a0049bb99610a817f1”,
“gasUsed”: 32688,
“bloom”: “00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000”,
“log”: []
}
}
2.solidity 实现
callcontract d3764a0b7fbbe2e39ee4adc3908b5b5dbea22c14 69bc0963dfa80e3294fd8806ab908904403db376b3dd35c6356ab2d3b884db4f6ec5e93d0000000000000000000000000000000000000000000000000000000000000020ca08c0813407de3a78053c976462eacbde3fd69843e21acf8dd636149bf4b7530731bce3ed9b489da0165af79759c1d586ef8fe53b3aab95fcab68d01ed6f156
{
“address”: “d3764a0b7fbbe2e39ee4adc3908b5b5dbea22c14”,
“executionResult”: {
“gasUsed”: 886077,
“excepted”: “None”,
“newAddress”: “d3764a0b7fbbe2e39ee4adc3908b5b5dbea22c14”,
“output”: “0000000000000000000000004fdff1b4bde5edf13360ff0946518a01115ce818”,
“codeDeposit”: 0,
“gasRefunded”: 0,
“depositSize”: 0,
“gasForDeposit”: 0,
“exceptedMessage”: “”
},
“transactionReceipt”: {
“stateRoot”: “5d9e1ad1b5d09e9e7c41d09078434488927366adf8ebf5a0049bb99610a817f1”,
“gasUsed”: 886077,
“bloom”: “00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000”,
“log”: []
}
}
可见预编译合约调用btc_ecrecover需要花费 32688 gas,而完全用 solidity 实现需要 886077 gas。预编译合约实现的 gas 花费远远少于 solidity 实现。
QIP-6 的影响
QIP-6 大大减小了智能合约开发者的开发成本。从调用合约的角度来说,如果完全用 solidity 在合约中实现 recover,其 gas 使用量远远超过btc_ecrecover函数。于是使用btc_ecrecover来获取消息签名地址的合约调用成本也大大降低。此外,QIP-6 也让 Qtum 的智能合约系统更加完备。
另一方面,QIP-6 没有对原有的ecrecover进行修改,保持了 Qtum 和以太坊的兼容性,理论上不会带来任何风险。