Qtum releases QIP-6, greatly reducing development costs through pre-compiled contracts

 

background

 

The ecrecover function in Ethereum can be used to obtain the address that signed a message. This is useful for proving that a message or a piece of data was signed by a specified account (rather than tampered with). But Qtum does not use Ethereum's account model, but uses Bitcoin's UTXO model. The address algorithm is also different from Ethereum, so this function does not apply to Qtum. In some cases where the signature source information needs to be verified, Qtum developers cannot easily complete this verification in a smart contract. Instead, they need to be fully implemented in the contract or call a contract that obtains the signer’s public key from the signature and message. This causes a very large overhead, which in turn makes the call cost of the corresponding contract very high.

 

 

Details of the problem

 

ecrecover accepts the hash of a message and the signature of the message, then calculates the public key corresponding to the signed private key, and converts the public key to the Ethereum address format. However, the address algorithm of Ethereum is different from Qtum, and what ecrecover returns is the result of hashing the public key. This process is irreversible, so this function cannot be used on Qtum.

In Ethereum, the address calculation method is as follows:

keccak256(pubkey)

 

On Qtum, the address calculation method is the same as Bitcoin, and the following calculation method is used:

ripemd160(sha256(pubkey))

 

In the Qtum contract, msg.sender is a Qtum address. Since every step of converting from a public key to an address is irreversible, the Ethereum address returned by ecrecover cannot be compared with the Qtum address in msg.sender. However, the existing Qtum smart contract does not provide any function to obtain the Qtum address from the message signature, which causes Qtum smart contract developers to develop or use Secp256k1 related libraries to calculate the signature public key and address, resulting in greater Calculation costs and higher contract fees.

 

Another detail that needs to be noted is that there are some subtle differences between the implementation of the Bitcoin message signature algorithm used by Qtum and the Ethereum message signature algorithm:

The Ethereum signature is composed of the following format:

[r][s][v]

 

The signature of Qtum is:

[v][r][s]

 

Where v is the recover id, r is the X coordinate of a point R on the elliptic curve, and s is the Y coordinate of this point R. The above differences lead to different implementation details of Qtum and Ethereum's recover algorithm.

 

 

QIP-6's solution

 

By adding a pre-compiled contract to the Qtum virtual machine, it provides an interface for calling the recover code in the Qtum core code. Smart contract developers only need to write a simple function or two to get the address of the signer from the signed message. The interface of the newly added precompiled contract is consistent with ecrecover.

 

What is a precompiled contract

 

Pre-compiled contracts are a compromise solution adopted by EVM in order to provide some more complex library functions that are not suitable for writing opcode (mostly used for complex calculations such as encryption and hashing). Because it is implemented with low-level code and has a fast execution speed, it consumes less for developers than directly using functions running on the EVM. Ethereum uses pre-compiled contracts to provide some commonly used and more cumbersome operations, such as sha256, ripemd160hash, etc.

 

Implementation of pre-compiled contracts

 

The core code of the pre-compiled contract is implemented by the bottom layer of the virtual machine (C++), which provides an interface for smart contract calls by registering to a fixed address manually designated during the initialization of the virtual machine.

 

Use of pre-compiled contracts

 

A typical calling method:

 

 

 

assembly {
if iszero(call(gasLimit, contractAddress, value, input, inputLength, output, outputLength)) {
revert(0, 0)
}
}

 

In the new version of the virtual machine, staticcall can also be used:

 

 

 

assembly {   success := staticcall(gasLimit, contractAddress, input, inputLength, output, outputLength)}

 

Among them, contractAddress is the address of the precompiled contract to be called. The address of btc_ecrecover newly added by Qtum this time is 0x85. input is the parameter list of the calling contract. The return value of this call represents whether the call was successful, 1 means success, and 0 means failure. The returned data will be written to the output.

Let's look at an example:

 

 

 

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]);
}
}

 

 

 

In the above example, just call the btc_ecrecover function to get the address of the message signer. In order to simplify the input, the example also encapsulates a recover function, so that the developer can complete the contract call as long as the original signature is passed in.

 

Below we do not use pre-compiled contracts, but completely use solidity code to implement the btc_ecrecover function. The code is implemented as follows:

 

 

 

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])
);
}
}

 

 

 

We deployed the above two contracts on the test chain, the addresses are as follows:

  • Pre-compiled contract: 21ea1d8376d1820d7091084a76f380143b59aaf8

  • Solidity implementation: 4fdff1b4bde5edf13360ff0946518a01115ce818

 

Use address

qQqip6i2e2buCZZNdqMw4VNpaYpnLm4JAx signs the message btc_ecrecover test, we get the call parameters of btc_ecrecover:

 

 

bytes32 msgh = 0xdfa80e3294fd8806ab908904403db376b3dd35c6356ab2d3b884db4f6ec5e93d
uint8 v = 0x20
bytes32 r = 0xca08c0813407de3a78053c976462eacbde3fd69843e21acf8dd636149bf4b753
bytes32 s = 0x0731bce3ed9b489da0165af79759c1d586ef8fe53b3aab95fcab68d01ed6f156

 

 

The call results of the two contracts are as follows:

 

1. Pre-compiled contract

 

 

 

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 implementation

 

 

 

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": []
}
}

 

 

 

It can be seen that calling btc_ecrecover in the pre-compiled contract requires 32688 gas, while the full implementation of solidity requires 886077 gas. The gas cost of pre-compiled contract implementation is far less than solidity implementation.

 

 

Impact of QIP-6

 

QIP-6 greatly reduces the development cost of smart contract developers. From the perspective of calling the contract, if solidity is used to implement recover in the contract, its gas usage far exceeds the btc_ecrecover function. Therefore, the contract call cost of using btc_ecrecover to obtain the message signature address is also greatly reduced. In addition, QIP-6 also makes Qtum's smart contract system more complete.

 

On the other hand, QIP-6 has not modified the original ecrecover, maintaining the compatibility of Qtum and Ethereum, and theoretically will not bring any risk.

 

Guess you like

Origin blog.csdn.net/weixin_42667079/article/details/101267471