Skip to content

在Moonbase Alpha上的以太坊主网预编译合约

Precompiled Contracts Banner

概览

Moonbase Alpha v2版本中新增的另外一个功能是提供以太坊上的一些原生预编译合约

目前这一功能包含以下预编译:ecrecover、sha256、sha3FIPS256、ripemd-160、Bn128Add、Bn128Mul、Bn128Pairing、恒等函数和模幂运算函数。

本教程中,我们将介绍如何使用和/或验证这些预编译。

查看先决条件

本教程操作需安装Node.js(我们将使用v15.x)和「npm package manager」。您可通过Node.js下载或自行运行以下代码完成安装

curl -sL https://deb.nodesource.com/setup_15.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

您可以通过请求每个安装包的版本来验证是否安装正确:

node -v
npm -v

撰写本教程时,所用版本分别为15.2.1和7.0.8。另外,我们还将需要通过执行以下命令安装Web3安装包:

npm install --save web3

您需要使用ls命令来验证所安装的Web3版本:

npm ls web3
撰写本教程时,所用版本为1.3.0。此外,我们还将使用Remix,并通过MetaMask将其连接至Moonbase Alpha测试网。

要在Moonbeam或Moonriver网络上测试本指南中的示例,您可以从受支持的网络端点提供商之一获取您自己的端点和API密钥。

使用ECRECOVER进行签名验证

这一预编译的主要功能是验证消息签名。一般来说,将某笔交易的签名值输入到ecrecover中,将会返回一个地址,如果该地址与发送该交易的公共地址相同,则签名通过验证。

我们用一个简单的例子来说明如何利用这一预编译功能。我们需要进行签名,然后获取包含这些数值的已签名消息,从而获得交易签名值(v, r, s):

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

这一代码将在终端返回以下对象:

{
  message: '0xc2ae6711c7a897c75140343cde1cbdba96ebbd756f5914fde5c12fadf002ec97',
  messageHash: '0xc51dac836bc7841a01c4b631fa620904fc8724d7f9f1d3c420f0e02adf229d50',
  v: '0x1b',
  r: '0x44287513919034a471a7dc2b2ed121f95984ae23b20f9637ba8dff471b6719ef',
  s: '0x7d7dc30309a3baffbfd9342b97d0e804092c0aeb5821319aa732bc09146eafb4',
  signature: '0x44287513919034a471a7dc2b2ed121f95984ae23b20f9637ba8dff471b6719ef7d7dc30309a3baffbfd9342b97d0e804092c0aeb5821319aa732bc09146eafb41b'
}

有了这些必要数值,我们就可以到Remix测试预编译合约。请注意,签名验证也可以通过Web3 JS库来实现,但在本示例中,我们将会使用Remix,以确保它使用的是区块链上的预编译合约。我们可以使用以下Solidity代码进行签名验证:

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

使用Remix编译器部署并将MetaMask连接至Moonbase Alpha即可部署合约。调用verify()方法进行验证,如果ecrecover返回的地址与消息签名所使用的地址(与密钥相关,需在合约中手动设置)一致,即返回true

使用SHA256函数获取哈希值

向这一函数输入数据可返回其SHA256哈希值。测试这一预编译合约,可以使用此在线工具来计算任何字符串的SHA256哈希值。在本示例中,我们将使用Hello World!。直接进入Remix并部署以下代码,计算出来的哈希值将在expectedHash变量中显示:

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

合约部署后,就可以调用checkHash()方法进行验证。如果calculateHash()返回的哈希值与所提供的哈希值一致,即返回true

使用SHA3FIPS256函数获取哈希值

SHA3-256是SHA-3安全散列算法(遵循FIPS202)系列的一部分,其算法的输出长度为256比特。尽管SHA-3的名称与SHA256相似,但SHA-3系列使用完全不同的算法构建,因此对于相同的输入产生的哈希输出与SHA256不同。您可以使用此在线工具自行验证。计算SHA3-256输出后,在下拉菜单将算法改为SHA256并记下结果输出。

目前Solidity暂不支持SHA3-256,因此需要使用内联汇编的方式调用这一函数。您可使用以下代码调用这个预编译合约。

pragma solidity >=0.4.21;

contract Precompiles {
    function sha3fips(bytes memory data) public view returns (bytes32) {
        bytes32[1] memory h;
        assembly {
            if iszero(
                staticcall(not(0), 0x400, add(data, 32), mload(data), h, 32)
            ) {
                invalid()
            }
        }
        return h[0];
    }
}

使用Remix编译器部署并将MetaMask连接至Moonbase Alpha即可部署合约。调用sha3fips(bytes memory data)方法返回数据参数的编码字符串。

使用RIPEMD-160函数获取哈希值

向这一函数输入数据可返回其RIPEMD-160哈希值。测试这一预编译合约,可以使用这个在线工具来计算任何字符串的RIPEMD-160哈希值。在本示例中,我们仍使用Hello World!。我们将使用相同的代码,但使用另一个函数:ripemd160函数。请注意,这个函数返回的是bytes20类型的变量:

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

合约部署后,就可以调用checkHash()方法进行验证。如果calculateHash()返回的哈希值与所提供的哈希值一致,即返回true

BN128Add

BN128Add预编译实现了原生椭圆曲线点添加。它返回一个表示(ax, ay) + (bx, by)的椭圆曲线点,这样(ax, ay)(bx, by)是曲线BN256上的有效点。

目前Solidity暂不支持BN128Add,因此需要使用内联汇编的方式调用这一函数。您可使用以下代码样本调用这个预编译合约。

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

使用Remix编译器部署并将MetaMask连接至Moonbase Alpha即可部署合约。调用callBn256Add(bytes32 ax, bytes32 ay, bytes32 bx, bytes32 by)方法返回操作结果。

BN128Mul

BN128Mul预编译实现了原生椭圆曲线的标量乘法。它返回一个椭圆曲线点,表示scalar * (x, y),使得(x, y)是曲线BN256上的有效曲线点。

目前Solidity暂不支持BN128Mul,因此需要使用内联汇编的方式调用这一函数。您可使用以下代码调用这个预编译合约。

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

使用Remix编译器部署并将MetaMask连接至Moonbase Alpha即可部署合约。调用callBn256ScalarMul(bytes32 x, bytes32 y, bytes32 scalar)方法返回操作结果。

BN128Pairing

BN128Pairing预编译通过椭圆曲线配对操作进行zkSNARK验证。更多信息请访问EIP-197

目前Solidity暂不支持BN128Pairing,因此需要使用内联汇编的方式调用这一函数。您可使用以下代码调用这个预编译合约。

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

使用Remix编译器部署并将MetaMask连接至Moonbase Alpha即可部署合约。调用function callBn256Pairing(bytes memory input)方法返回操作结果。

恒等函数

这一函数也被称为“数据复制”函数,是复制内存数据性价比较高的方法。

目前Solidity暂不支持恒等函数,因此需要使用内联汇编的方式调用这一函数。您可以使用以下代码(经修改已适应Solidity)调用这个预编译合约。我们可以使用此在线工具来获取任何字符串的字节码,因为它是callDataCopy()方法的输入值。

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

合约部署后,就可以调用callDataCopy()方法来验证memoryStored是否与您在函数中所输入的字节码相符。

模幂运算函数

我们要介绍的最后预编译合约主要用于计算整数b(基数)乘以e次方(指数)并除以一个正整数m(除数)后的余数。

目前Solidity暂不支持模幂运算函数,因此需要使用内联汇编的方式调用这一函数。以下代码经过了简化,能更好地呈现这一预编译合约的功能。

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

您也可以在Remix环境中尝试使用这一合约。调用verify()函数,输入基数、指数和除数,结果将储存在函数的checkResult变量中。