以太坊包含了一些用于密码学计算的预编译合约,可以用来实现高级隐私保护功能。在这个教程中我们将了解以太坊提供的预编译合约清单,并通过bn256ScalarMul
和bigModExp
这两个实例学习以太坊预编译合约的使用方法。
用自己熟悉的语言学习以太坊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客户端的预编译合约清单看起来像这样:
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的代码:
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支付内存利用的成本。我们需要调用预编译合约的值保持在这个内存的顶部。
我们可以将之前保存在持久化存储中的变量赋值给内存,方式如下:
uint256[2] memory inputToPrecompile;
input[0] = somePreviouslyStoredValue;
input[1] = someOtherPreviouslyStoredValue;
这实际上就是我们在ecmul中的开始4行的操作。我们将值ax,ay,k压入虚拟栈的顶部。然后通过调用bn256ScalarMul预编译合约的地址就完成调用了。看下一部分的代码:
assembly {
if iszero(staticcall(gas, 0x07, input, 0x60, p, 0x40)) {
revert(0,0)
}
}
staticcall操作码的调用形式如下:
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预编译合约:
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)
来初始化内存指针。
原文链接:以太坊预编译合约使用教程 — 汇智网
来源:oschina
链接:https://my.oschina.net/u/3843525/blog/4300696