以太坊预编译合约使用教程

以太坊包含了一些用于密码学计算的预编译合约,可以用来 实现高级隐私保护功能。在这个教程中我们将了解以太坊 提供的预编译合约清单,并通过bn256ScalarMulbigModExp这两个实例学习以太坊预编译合约的使用方法。

用自己熟悉的语言学习以太坊DApp开发: Java | Php | Python | .Net / C# | Golang | Node.JS | Flutter / Dart

1、以太坊虚拟机基本概念

在继续下面的教程之前,我们需要对以太坊和Solidity有一些基本 的了解。我们关心的重点在于,以太坊有一个分布式的虚拟机即EVM, EVM提供了一组指令可以用于在区块链上执行交易并更新状态。关于EVM 的一些基本概念如下:

  • storage:可以永久在链上存储信息
  • memory:EVM虚拟机的工作内存,用于保存计算过程中的变量内容
  • uint:uint256类型的别名,可保存256位,完美匹配椭圆曲线坐标的要求
  • public:用来声明函数位公开可调用
  • view:用来告诉编译器,所装饰的函数不会修改合约状态
  • pure:表示所装饰的函数不涉及合约状态的读写

2、以太坊预编译合约清单

以太坊Geth客户端的预编译合约清单看起来像这样:

1
2
3
4
5
6
7
8
9
10
var PrecompiledContractsByzantium = map[common.Address]PrecompiledContract{     
common.BytesToAddress([]byte{1}): &ecrecover{},
common.BytesToAddress([]byte{2}): &sha256hash{},
common.BytesToAddress([]byte{3}): &ripemd160hash{},
common.BytesToAddress([]byte{4}): &dataCopy{},
common.BytesToAddress([]byte{5}): &bigModExp{},
common.BytesToAddress([]byte{6}): &bn256Add{},
common.BytesToAddress([]byte{7}): &bn256ScalarMul{},
common.BytesToAddress([]byte{8}): &bn256Pairing{},
}

上述代码中的映射结构记录了预编译合约的地址,是最后4个是新增的预编译 合约:

bigModExp:地址0x05,执行操作:b^e mod m。bigModExp预编译合约的输入为: 底数长度、指数长度、模长度、底数即b的值、指数即e的值、模即m的值

bn256Add:地址0x06,执行操作:(x1, y1) + (x2, y2),其中x1, y1, x2, y2 都是256位的域成员,因此 (x1, y1)和 (x2, y2)都是bn256曲线上的有效点,满足公式y^2 = x^3 + 3 mod fieldOrder。 bn256预编译合约的输入就是x1, y1, x2, y2。

bn256ScalarMul:地址0x07,执行操作:k * (x, y),其中k属于群,(x,y)是曲线上的有效点。 bn256scalarMul的输入是x, y, k。

bn256Pairing:地址0x08,执行操作:配对检查e(g1, g2) = e(-h1, h2,其中g1和h1属于群G1, g2和h2属于群G2。bn256Pairing可以接收任意多对椭圆曲线上的点。群G1上的点形式为(x,y),群 G2上的点形式为(ai + b, ci + d),其中a, b, c, d (依次为虚部、实部、虚部、实部) 需要在预编译 调用时传入。bn256Pairing代码首先检查已经送出6的倍数个成员,然后执行配对检查。

x, y, a, b, c, d的值都是域成员,因此都会按域大小取模。在bn256ScalarMul中使用的k 的值,则是按椭圆曲线群的阶取模。

下面我们将要学习两个主要的示例:bn256ScalarMul和bigModExp。bn256ScalarMul操作 和bn256Add非常类似,而bn256Pairing操作则更像bigModExp,因为这两者都接受可变长度 的输入,因此调用时需要指定输入大小。下面是调用bn256ScalarMul的代码:

1
2
3
4
5
6
7
8
9
10
11
12
function ecmul(uint ax, uint ay, uint k) public view returns(uint[2] memory p) { uint[3] memory input;
input[0] = ax;
input[1] = ay;
input[2] = k;

assembly {
if iszero(staticcall(gas, 0x07, input, 0x60, p, 0x40)) {
revert(0,0)
}
}
return p;
}

目前内联汇编已经支持if语句,调用时设置gas数量也比以前简单 —— 在调用时 使用gas,就表示利用所有可用gas,这避免了我们自己猜测需要的gas数量。

revert操作码将回滚所有的状态变化,起作用是在gas不足时或对预编译合约的 调用发生故障后,可以回滚部分完成的状态更新。

3、调用bn256ScalarMul预编译合约

每个地址关联的持久化内存被称为存储(Storage),这时一个key-value库, 实现从256位到256位数据的映射。在合约内这个键值库没有办法枚举,合约 也不能访问其他地址关联的存储。

如果采用如下形式初始化变量:uint256 blah,那么就会将变量blah保存到 持久化存储。uint是uint256的别名,如果需要更细粒度的管理,可以使用uint8, uint16等等。

EVM有一个虚拟栈可以保存256位的值。选择256位的目的是与密码学操作保持 兼容。所有的EVM操作都是利用这个虚拟栈完成的,它最多可以容纳1024个成员。 你可以拷贝栈顶16个成员之一,或者两两交换。所有其他的操作码都利用栈顶 特定位置的成员作为输入并将结果压入栈。

对于每一个消息调用,易失内存都被复位,内存以32字节为单位分配,使用gas 支付内存利用的成本。我们需要调用预编译合约的值保持在这个内存的顶部。

我们可以将之前保存在持久化存储中的变量赋值给内存,方式如下:

1
2
3
uint256[2] memory inputToPrecompile;
input[0] = somePreviouslyStoredValue;
input[1] = someOtherPreviouslyStoredValue;

这实际上就是我们在ecmul中的开始4行的操作。我们将值ax,ay,k压入虚拟栈的 顶部。然后通过调用bn256ScalarMul预编译合约的地址就完成调用了。看下一部分 的代码:

1
2
3
4
5
assembly {
if iszero(staticcall(gas, 0x07, input, 0x60, p, 0x40)) {
revert(0,0)
}
}

staticcall操作码的调用形式如下:

1
staticcall(gasLimit, to, inputOffset, inputSize, outputOffset, outputSize)

可以看到在上面的调用bn256ScalarMul的代码中,我们:

  • 在扣除2000后,发送当前可用的gas
  • 调用地址0x07的预编译合约,这对应bn256ScalarMul
  • 使用内存变量input作为输入偏移参数
  • 将输入大小声明为0x60,这对应3个256位数值,表示一个椭圆曲线点和一个256位标量
  • 将输出保存在p中
  • 输出大小为0x40,对应要返回的椭圆曲线点

这样就完成了对以太坊预编译合约bn256ScalarMul的调用,ecmul函数的返回值现在就是 bn256ScalarMul预编译合约的返回值!

4、调用bigModExp预编译合约

下面的代码调用bigModExp预编译合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function expmod(uint base, uint e, uint m) public view returns (uint o) {

assembly {
// define pointer
let p := mload(0x40)
// store data assembly-favouring ways
mstore(p, 0x20) // Length of Base
mstore(add(p, 0x20), 0x20) // Length of Exponent
mstore(add(p, 0x40), 0x20) // Length of Modulus
mstore(add(p, 0x60), base) // Base
mstore(add(p, 0x80), e) // Exponent
mstore(add(p, 0xa0), m) // Modulus
if iszero(staticcall(sub(gas, 2000), 0x05, p, 0xc0, p, 0x20)) {
revert(0, 0)
}
// data
o := mload(p)
}}

需要注意的是,0x40始终是空闲内存,因此可以使用p:=mload(0x40)来 初始化内存指针。


原文链接:precompiles & solidity

汇智网翻译整理,转载请标明出处