背 景以太坊中的ecrecover函數(shù)可以用來(lái)獲取對(duì)一條消息簽名的地址。這對(duì)于證明一條消息或者一段數(shù)據(jù)被一個(gè)指定的賬戶簽名過(guò)(而不是被篡改過(guò))
背 景
以太坊中的ecrecover函數(shù)可以用來(lái)獲取對(duì)一條消息簽名的地址。這對(duì)于證明一條消息或者一段數(shù)據(jù)被一個(gè)指定的賬戶簽名過(guò)(而不是被篡改過(guò))非常有用。但是 Qtum 沒有使用以太坊的賬戶模型,而是采用比特幣的 UTXO 模型,地址的算法也和以太坊不同,因此這個(gè)函數(shù)并不適用于 Qtum。
在一些需要驗(yàn)證簽名來(lái)源信息的情況下, Qtum 開發(fā)者并不能方便的在智能合約中完成這個(gè)驗(yàn)證,而是需要在合約中完整實(shí)現(xiàn)或者調(diào)用一次從簽名和消息獲取簽名者公鑰的合約,會(huì)造成非常大的開銷,進(jìn)而使得相應(yīng)合約的調(diào)用費(fèi)用非常高。
問(wèn)題的細(xì)節(jié)
ecrecover接受一個(gè)消息的哈希和消息的簽名,然后計(jì)算出簽名的私鑰對(duì)應(yīng)的公鑰,并將該公鑰轉(zhuǎn)換為以太坊地址格式。然而以太坊的地址算法和 Qtum 不同,而且ecrecover返回的是公鑰經(jīng)過(guò)哈希以后的結(jié)果,這個(gè)過(guò)程不可逆,因此在 Qtum 上無(wú)法使用這個(gè)函數(shù)。
在以太坊中,地址計(jì)算方法如下:
keccak256(pubkey)
而在 Qtum 上,地址的計(jì)算方式和比特幣相同,使用如下計(jì)算方法:
ripemd160(sha256(pubkey))
在 Qtum 的合約中,msg.sender是一個(gè) Qtum 地址。由于從公鑰開始轉(zhuǎn)換為地址的每一步操作都是不可逆的,ecrecover返回的以太坊地址無(wú)法和msg.sender中的 Qtum 地址進(jìn)行比較。而現(xiàn)有的 Qtum 智能合約中并沒有提供任何函數(shù)來(lái)從消息簽名中獲取 Qtum 地址,這導(dǎo)致 Qtum 智能合約開發(fā)者們不得不開發(fā)或使用Secp256k1相關(guān)的庫(kù)來(lái)計(jì)算簽名公鑰和地址,造成更大的計(jì)算開銷和更高的合約費(fèi)用。
另一個(gè)需要注意的細(xì)節(jié)是,Qtum 沿用的比特幣消息簽名算法和以太坊的消息簽名算法的實(shí)現(xiàn)上有一些細(xì)微的差別:
以太坊的簽名按如下格式組成:
[r][s][v]
而 Qtum 的簽名則是:
[v][r][s]
其中v是 recover id,r是橢圓曲線上的一個(gè)點(diǎn)R的X坐標(biāo),s是這個(gè)點(diǎn)R的Y坐標(biāo)。如上的不同導(dǎo)致 Qtum 和以太坊的 recover 算法的實(shí)現(xiàn)細(xì)節(jié)也不相同。
QIP-6 的解決方案
通過(guò)在 Qtum 的虛擬機(jī)中增加一個(gè)預(yù)編譯的合約,以提供一個(gè)用來(lái)調(diào)用 Qtum 核心代碼中的 recover 代碼的接口。智能合約開發(fā)者只需要寫簡(jiǎn)單的一兩個(gè)函數(shù)就能從簽名消息中獲取到簽名者的地址。新增的預(yù)編譯合約的接口和ecrecover保持一致。
什么是預(yù)編譯合約
預(yù)編譯合約是 EVM 中為了提供一些不適合寫成 opcode 的較為復(fù)雜的庫(kù)函數(shù)(多數(shù)用于加密、哈希等復(fù)雜計(jì)算)而采用的一種折中方案。由于它是用底層代碼實(shí)現(xiàn)的,執(zhí)行速度快,對(duì)于開發(fā)者來(lái)說(shuō)就比直接用運(yùn)行在 EVM 上的函數(shù)消耗更低。以太坊中使用預(yù)編譯合約提供一些常用的較為繁瑣的操作,比如sha256、ripemd160hash等。
預(yù)編譯合約的實(shí)現(xiàn)
預(yù)編譯合約的核心代碼由虛擬機(jī)底層(C++)實(shí)現(xiàn),通過(guò)在虛擬機(jī)的初始化過(guò)程中注冊(cè)到人為指定的固定地址上來(lái)提供智能合約調(diào)用的接口。
預(yù)編譯合約的使用
一個(gè)典型的調(diào)用方式:
assembly {
if iszero(call(gasLimit, contractAddress, value, input, inputLength, output, outputLength)) {
revert(0, 0)
}
}
在新版本的虛擬機(jī)中,還可以使用staticcall:
assembly {
success := staticcall(gasLimit, contractAddress, input, inputLength, output, outputLength)
}
其中contractAddress就是要調(diào)用的預(yù)編譯合約的地址,本次 Qtum 新增的 btc_ecrecover 的地址是0x85。input是調(diào)用合約的參數(shù)列表。這個(gè)調(diào)用的返回值代表了調(diào)用是否成功,1表示成功,0表示失敗。而返回的數(shù)據(jù)會(huì)寫入到output里面。
下面我們看一個(gè)例子:
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]);
}
}
在上面這個(gè)例子中,只要調(diào)用btc_ecrecover函數(shù)就能獲取到消息簽名者的地址。為了簡(jiǎn)化輸入,例子中也封裝了一個(gè)recover函數(shù),使得開發(fā)者只要傳入原始簽名就能完成合約調(diào)用。
下面我們不使用預(yù)編譯合約,而是完全使用 solidity 代碼實(shí)現(xiàn) btc_ecrecover功能。代碼實(shí)現(xiàn)如下:
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])
);
}
}
我們?cè)跍y(cè)試鏈上部署了上述兩個(gè)兩個(gè)合約,地址分別如下:
預(yù)編譯合約: 21ea1d8376d1820d7091084a76f380143b59aaf8
solidity實(shí)現(xiàn): 4fdff1b4bde5edf13360ff0946518a01115ce818
使用地址
qQqip6i2e2buCZZNdqMw4VNpaYpnLm4JAx對(duì)消息btc_ecrecover test進(jìn)行簽名,我們得到btc_ecrecover 的調(diào)用參數(shù):
bytes32 msgh = 0xdfa80e3294fd8806ab908904403db376b3dd35c6356ab2d3b884db4f6ec5e93d
uint8 v = 0x20
bytes32 r = 0xca08c0813407de3a78053c976462eacbde3fd69843e21acf8dd636149bf4b753
bytes32 s = 0x0731bce3ed9b489da0165af79759c1d586ef8fe53b3aab95fcab68d01ed6f156
兩個(gè)合約調(diào)用調(diào)用結(jié)果如下:
1.預(yù)編譯合約
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 實(shí)現(xiàn)
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": []
}
}
可見預(yù)編譯合約調(diào)用btc_ecrecover需要花費(fèi) 32688 gas,而完全用 solidity 實(shí)現(xiàn)需要 886077 gas。預(yù)編譯合約實(shí)現(xiàn)的 gas 花費(fèi)遠(yuǎn)遠(yuǎn)少于 solidity 實(shí)現(xiàn)。
QIP-6 的影響
QIP-6 大大減小了智能合約開發(fā)者的開發(fā)成本。從調(diào)用合約的角度來(lái)說(shuō),如果完全用 solidity 在合約中實(shí)現(xiàn) recover,其 gas 使用量遠(yuǎn)遠(yuǎn)超過(guò)btc_ecrecover函數(shù)。于是使用btc_ecrecover來(lái)獲取消息簽名地址的合約調(diào)用成本也大大降低。此外,QIP-6 也讓 Qtum 的智能合約系統(tǒng)更加完備。
另一方面,QIP-6 沒有對(duì)原有的ecrecover進(jìn)行修改,保持了 Qtum 和以太坊的兼容性,理論上不會(huì)帶來(lái)任何風(fēng)險(xiǎn)。(Qtum量子鏈)