Ethereum MainNet Precompiled Contracts¶
Introduction¶
Precompiled contracts in Ethereum are contracts that include complex cryptographic computations, but do not require the overhead of the EVM. These precompiles can be used within the EVM to handle specific common operations such as hashing and signature schemes.
The following precompiles are currently included: ecrecover, sha256, ripemd-160, Bn128Add, Bn128Mul, Bn128Pairing, the identity function, and modular exponentiation.
These precompiles are natively available on Ethereum and, to maintain Ethereum compatibility, they are also available on Moonbeam.
In this guide, you will learn how to use and/or verify these precompiles.
Checking Prerequisites¶
You need to install Node.js (for this example, you can use v16.x) and the npm package manager. You can download directly from Node.js or in your terminal:
curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash -
sudo apt install -y nodejs
# You can use homebrew (https://docs.brew.sh/Installation)
brew install node
# Or you can use nvm (https://github.com/nvm-sh/nvm)
nvm install node
You can verify that everything is installed correctly by querying the version for each package:
node -v
npm -v
As of writing this guide, the versions used were 15.2.1 and 7.0.8, respectively. You will also need to install the Web3 package by executing:
npm install --save web3
To verify the installed version of Web3, you can use the ls
command:
npm ls web3
As of writing this guide, the version used was 1.3.0. You will be also using Remix, connecting it to the Moonbase Alpha TestNet via MetaMask.
To test out the examples in this guide on Moonbeam or Moonriver, you will need to have your own endpoint and API key, which you can get from one of the supported Endpoint Providers.
Verify Signatures with ECRECOVER¶
The main function of this precompile is to verify the signature of a message. In general terms, you feed ecrecover
the transaction's signature values and it returns an address. The signature is verified if the address returned is the same as the public address that sent the transaction.
The following will be a small example to showcase how to leverage this precompiled function. You'll need to retrieve the transaction's signature values (v
, r
, s
). Therefore, you'll sign and retrieve the signed message where these values are:
const { Web3 } = require('web3');
// Provider
const web3 = new Web3('https://rpc.api.moonbase.moonbeam.network');
// Address and Private Key
const address = '0x6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b';
const pk1 = '99B3C12287537E38C90A9219D4CB074A89A16E9CDB20BF85728EBD97C343E342';
const msg = web3.utils.sha3('supercalifragilisticexpialidocious');
async function signMessage(pk) {
try {
// Sign and get Signed Message
const smsg = await web3.eth.accounts.sign(msg, pk);
console.log(smsg);
} catch (error) {
console.error(error);
}
}
signMessage(pk1);
This code will return the following object in the terminal:
{
message: '0xc2ae6711c7a897c75140343cde1cbdba96ebbd756f5914fde5c12fadf002ec97',
messageHash: '0xc51dac836bc7841a01c4b631fa620904fc8724d7f9f1d3c420f0e02adf229d50',
v: '0x1b',
r: '0x44287513919034a471a7dc2b2ed121f95984ae23b20f9637ba8dff471b6719ef',
s: '0x7d7dc30309a3baffbfd9342b97d0e804092c0aeb5821319aa732bc09146eafb4',
signature: '0x44287513919034a471a7dc2b2ed121f95984ae23b20f9637ba8dff471b6719ef7d7dc30309a3baffbfd9342b97d0e804092c0aeb5821319aa732bc09146eafb41b'
}
With the necessary values, you can go to Remix to test the precompiled contract. Note that this can also be verified with the Web3.js library, but in this case, you can go to Remix to be sure that it is using the precompiled contract on the blockchain. The Solidity code you can use to verify the signature is the following:
pragma solidity ^0.7.0;
contract ECRECOVER {
address addressTest = 0x12Cb274aAD8251C875c0bf6872b67d9983E53fDd;
bytes32 msgHash =
0xc51dac836bc7841a01c4b631fa620904fc8724d7f9f1d3c420f0e02adf229d50;
uint8 v = 0x1b;
bytes32 r =
0x44287513919034a471a7dc2b2ed121f95984ae23b20f9637ba8dff471b6719ef;
bytes32 s =
0x7d7dc30309a3baffbfd9342b97d0e804092c0aeb5821319aa732bc09146eafb4;
function verify() public view returns (bool) {
// Use ECRECOVER to verify address
return (ecrecover(msgHash, v, r, s) == (addressTest));
}
}
Using the Remix compiler and deployment and with MetaMask pointing to Moonbase Alpha, you can deploy the contract and call the verify()
method that returns true if the address returned by ecrecover
is equal to the address used to sign the message (related to the private key and needs to be manually set in the contract).
Hashing with SHA256¶
This hashing function returns the SHA256 hash from the given data. To test this precompile, you can use this SHA256 Hash Calculator tool to calculate the SHA256 hash of any string you want. In this case, you'll do so with Hello World!
. You can head directly to Remix and deploy the following code, where the calculated hash is set for the expectedHash
variable:
pragma solidity ^0.7.0;
contract Hash256 {
bytes32 public expectedHash =
0x7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069;
function calculateHash() internal pure returns (bytes32) {
string memory word = "Hello World!";
bytes32 hash = sha256(bytes(word));
return hash;
}
function checkHash() public view returns (bool) {
return (calculateHash() == expectedHash);
}
}
Once the contract is deployed, you can call the checkHash()
method that returns true if the hash returned by calculateHash()
is equal to the hash provided.
Hashing with RIPEMD160¶
This hashing function returns a RIPEMD160 hash from the given data. To test this precompile, you can use this RIPEMD160 Hash Calculator tool to calculate the RIPEMD160 hash of any string. In this case, you'll do so again with Hello World!
. You'll reuse the same code as before, but use the ripemd160
function. Note that it returns a bytes20
type variable:
pragma solidity ^0.7.0;
contract HashRipmd160 {
bytes20 public expectedHash = hex"8476ee4631b9b30ac2754b0ee0c47e161d3f724c";
function calculateHash() internal pure returns (bytes20) {
string memory word = "Hello World!";
bytes20 hash = ripemd160(bytes(word));
return hash;
}
function checkHash() public view returns (bool) {
return (calculateHash() == expectedHash);
}
}
With the contract deployed, you can call the checkHash()
method that returns true if the hash returned by calculateHash()
is equal to the hash provided.
BN128Add¶
The BN128Add precompile implements a native elliptic curve point addition. It returns an elliptic curve point representing (ax, ay) + (bx, by)
such that (ax, ay)
and (bx, by)
are valid points on the curve BN256.
Currently there is no BN128Add support in Solidity, so it needs to be called with inline assembly. The following sample code can be used to call this precompile.
pragma solidity >=0.4.21;
contract Precompiles {
function callBn256Add(
bytes32 ax,
bytes32 ay,
bytes32 bx,
bytes32 by
) public returns (bytes32[2] memory result) {
bytes32[4] memory input;
input[0] = ax;
input[1] = ay;
input[2] = bx;
input[3] = by;
assembly {
let success := call(gas, 0x06, 0, input, 0x80, result, 0x40)
switch success
case 0 {
revert(0, 0)
}
}
}
}
Using the Remix compiler and deployment and with MetaMask pointing to Moonbase Alpha, you can deploy the contract and call the callBn256Add(bytes32 ax, bytes32 ay, bytes32 bx, bytes32 by)
method to return the result of the operation.
BN128Mul¶
The BN128Mul precompile implements a native elliptic curve multiplication with a scalar value. It returns an elliptic curve point representing scalar * (x, y)
such that (x, y)
is a valid curve point on the curve BN256.
Currently there is no BN128Mul support in Solidity, so it needs to be called with inline assembly. The following sample code can be used to call this precompile.
pragma solidity >=0.4.21;
contract Precompiles {
function callBn256ScalarMul(
bytes32 x,
bytes32 y,
bytes32 scalar
) public returns (bytes32[2] memory result) {
bytes32[3] memory input;
input[0] = x;
input[1] = y;
input[2] = scalar;
assembly {
let success := call(gas, 0x07, 0, input, 0x60, result, 0x40)
switch success
case 0 {
revert(0, 0)
}
}
}
}
Using the Remix compiler and deployment and with MetaMask pointing to Moonbase Alpha, you can deploy the contract and call the callBn256ScalarMul(bytes32 x, bytes32 y, bytes32 scalar)
method to return the result of the operation.
BN128Pairing¶
The BN128Pairing precompile implements elliptic curve pairing operation to perform zkSNARK verification. For more information, check out the EIP-197 standard.
Currently there is no BN128Pairing support in Solidity, so it needs to be called with inline assembly. The following sample code can be used to call this precompile.
pragma solidity >=0.4.21;
contract Precompiles {
function callBn256Pairing(
bytes memory input
) public returns (bytes32 result) {
// input is a serialized bytes stream of (a1, b1, a2, b2, ..., ak, bk) from (G_1 x G_2)^k
uint256 len = input.length;
require(len % 192 == 0);
assembly {
let memPtr := mload(0x40)
let success := call(
gas(),
0x08,
0,
add(input, 0x20),
len,
memPtr,
0x20
)
switch success
case 0 {
revert(0, 0)
}
default {
result := mload(memPtr)
}
}
}
}
Using the Remix compiler and deployment and with MetaMask pointing to Moonbase Alpha, you can deploy the contract and call the function callBn256Pairing(bytes memory input)
method to return the result of the operation.
The Identity Function¶
Also known as datacopy, this function serves as a cheaper way to copy data in memory.
Currently there is no Identity Function support in Solidity, so it needs to be called with inline assembly. The following sample code (adapted to Solidity), can be used to call this precompiled contract:
pragma solidity ^0.7.0;
contract Identity {
bytes public memoryStored;
function callDatacopy(bytes memory data) public returns (bytes memory) {
bytes memory result = new bytes(data.length);
assembly {
let len := mload(data)
if iszero(
call(
gas(),
0x04,
0,
add(data, 0x20),
len,
add(result, 0x20),
len
)
) {
invalid()
}
}
memoryStored = result;
return result;
}
}
You can use this Web3 Type Converter tool to get bytes from any string, as this is the input of the callDataCopy()
method.
With the contract deployed, you can call the callDataCopy()
method and verify if memoryStored
matches the bytes that you pass in as an input of the function.
Modular Exponentiation¶
This precompile calculates the remainder when an integer b
(base) is raised to the e
-th power (the exponent), and is divided by a positive integer m
(the modulus).
The Solidity compiler does not support it, so it needs to be called with inline assembly. The following code was simplified to show the functionality of this precompile:
pragma solidity ^0.7.0;
contract ModularCheck {
uint public checkResult;
// Function to Verify ModExp Result
function verify(uint _base, uint _exp, uint _modulus) public {
checkResult = modExp(_base, _exp, _modulus);
}
function modExp(
uint256 _b,
uint256 _e,
uint256 _m
) public returns (uint256 result) {
assembly {
// Free memory pointer
let pointer := mload(0x40)
// Define length of base, exponent and modulus. 0x20 == 32 bytes
mstore(pointer, 0x20)
mstore(add(pointer, 0x20), 0x20)
mstore(add(pointer, 0x40), 0x20)
// Define variables base, exponent and modulus
mstore(add(pointer, 0x60), _b)
mstore(add(pointer, 0x80), _e)
mstore(add(pointer, 0xa0), _m)
// Store the result
let value := mload(0xc0)
// Call the precompiled contract 0x05 = bigModExp
if iszero(call(not(0), 0x05, 0, pointer, 0xc0, value, 0x20)) {
revert(0, 0)
}
result := mload(value)
}
}
}
You can try this in Remix. Use the function verify()
, passing the base, exponent, and modulus. The function will store the value in the checkResult
variable.
P256 Verify¶
The P256Verify Precompile adds support for RIP-7212, signature verification for Secp256r1 elliptic curve. This precompile adds a WASM implementation of the signature verification and is intended to be replaced by a native runtime function call once available.
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.3;
contract P256Verify {
function verify(
bytes32 msg_hash,
bytes32[2] memory signature,
bytes32[2] memory public_key
) public view returns (bool) {
bool output;
bytes memory args = abi.encodePacked(
msg_hash,
signature[0],
signature[1],
public_key[0],
public_key[1]
);
bool success;
assembly {
success := staticcall(not(0), 0x100, add(args, 32), mload(args), output, 0x20)
}
require(success, "p256verify precompile call failed");
return output;
}
}
The file below contains two different test cases: one with a valid signature test and a second with an invalid signature test.
p256verifywithtests.sol
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.3;
contract P256Verify {
function verify(
bytes32 msg_hash,
bytes32[2] memory signature,
bytes32[2] memory public_key
) public view returns (bool) {
bool output;
bytes memory args = abi.encodePacked(
msg_hash,
signature[0],
signature[1],
public_key[0],
public_key[1]
);
bool success;
assembly {
success := staticcall(not(0), 0x100, add(args, 32), mload(args), output, 0x20)
}
require(success, "p256verify precompile call failed");
return output;
}
function test() public {
bytes32[2] memory msg_hashes;
bytes32[2][2] memory signatures;
bytes32[2][2] memory public_keys;
bool[2] memory expected_result;
// Case 1 (valid)
msg_hashes[0] = hex"b5a77e7a90aa14e0bf5f337f06f597148676424fae26e175c6e5621c34351955";
signatures[0][0] = hex"289f319789da424845c9eac935245fcddd805950e2f02506d09be7e411199556";
signatures[0][1] = hex"d262144475b1fa46ad85250728c600c53dfd10f8b3f4adf140e27241aec3c2da";
public_keys[0][0] = hex"3a81046703fccf468b48b145f939efdbb96c3786db712b3113bb2488ef286cdc";
public_keys[0][1] = hex"ef8afe82d200a5bb36b5462166e8ce77f2d831a52ef2135b2af188110beaefb1";
expected_result[0] = true;
// Case 2 (invalid)
msg_hashes[1] = hex"d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b";
signatures[1][0] = hex"6162630000000000000000000000000000000000000000000000000000000000";
signatures[1][1] = hex"6162630000000000000000000000000000000000000000000000000000000000";
public_keys[1][0] = hex"6162630000000000000000000000000000000000000000000000000000000000";
public_keys[1][1] = hex"6162630000000000000000000000000000000000000000000000000000000000";
expected_result[0] = false;
for (uint256 i = 0; i < expected_result.length; i++) {
bool result = verify(msg_hashes[i], signatures[i], public_keys[i]);
if (expected_result[i]) {
require(result, "Expected success");
} else {
require(!result, "Expected failure");
}
}
}
}
Using the Remix compiler and deployment and with MetaMask pointing to Moonbase Alpha, you can deploy the contract and call the verify
method with the following parameters:
Parameter | Value |
---|---|
msg_hash | 0xb5a77e7a90aa14e0bf5f337f06f597148676424fae26e175c6e5621c34351955 |
signature | ["0x289f319789da424845c9eac935245fcddd805950e2f02506d09be7e411199556", "0xd262144475b1fa46ad85250728c600c53dfd10f8b3f4adf140e27241aec3c2da"] |
public_key | ["0x3a81046703fccf468b48b145f939efdbb96c3786db712b3113bb2488ef286cdc", "0xef8afe82d200a5bb36b5462166e8ce77f2d831a52ef2135b2af188110beaefb1"] |
Expected Result | true |
Parameter | Value |
---|---|
msg_hash | 0xd182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b |
signature | ["0x6162630000000000000000000000000000000000000000000000000000000000", "0x6162630000000000000000000000000000000000000000000000000000000000"] |
public_key | ["0x6162630000000000000000000000000000000000000000000000000000000000", "0x6162630000000000000000000000000000000000000000000000000000000000"] |
Expected Result | false |
You'll receive two booleans in response; the first one indicates whether the signature was valid, and the second indicates whether the call to the P256Verify precompile was successful. The second boolean should always return true; the first is the one to check to see if the signature is valid.
| Created: October 9, 2020