Skip to content

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.

Last update: October 31, 2024
| Created: October 9, 2020