背 景以太坊中的ecrecover函數可以用來獲取對一條消息簽名的地址。這對于證明一條消息或者一段數據被一個指定的賬戶簽名過(而不是被篡改過)
背 景
以太坊中的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 和以太坊的兼容性,理論上不會帶來任何風險。(Qtum量子鏈)