作者:高德福瑞 | 来源:互联网 | 2023-09-12 21:23
本篇主要介绍 Curve25519-dalek 库中的批量求逆运算,并会给出批量求逆和普通求逆运算的效率对比。## Introduction批量求逆的思想可以应用到任何一种群结构上,比如 Curve2
本篇主要介绍 Curve25519-dalek 库中的批量求逆运算,并会给出批量求逆和普通求逆运算的效率对比。
## Introduction
批量求逆的思想可以应用到任何一种群结构上,比如 Curve25519-dalek 库中的 scalar 求逆、底层有限域上元素的求逆等。在这里我们将以 Curve25519-dalek 库中的 scalar 求逆运算为例来介绍该算法的核心思想。
Curve25519-dalek 库中的 scalar 定义如下:
```rust
pub struct Scalar {
pub(crate) bytes: [u8; 32],
}
```
该 scalar 是由 32 个字节数组表示的,由于是针对 curve25519 曲线定义的,因此 scalar 的取值范围是`[0,l)`,其中`l`是相应椭圆曲线群的阶: $l = 2^{252} + 27742317777372353535851937790883648493$. 并且所有的标量运算都是模`l`的条件下进行的。
在这里可以将所有 scalar(0 除外)组成的集合作为一个乘法群,群中运算为模`l`的乘法运算。并且满足封闭性,结合律。单位元为 1,每个元素 $x$ 都存在相应逆 $x^{-1}$,也就是关于 1 的乘法逆: $x*x^{-1}=1$ $mod$ $l$。由于`l`是一个素数,根据费马小定理,可以知道 $x^{l-1}=1 mod l$,因此 $x*x^{l-2}=1 mod l$,也就是说 $x^{-1}=x^{l-2} mod l$。因此存在有效计算群上元素乘法逆的算法,记为 invert。
在这个前提条件下,我们研究如何进行批量求逆运算,即对于一个 scalar 的数组,如果高效计算该数组中所有元素的逆元。一个最基本的算法就是利用 invert 算法对数组中的 scalar 挨个求逆,但前面我们看到 invert 算法需要对元素进行`l-2`次的指数运算,而`l-2`本身是一个比较大的数,所以运算起来比较费时。因此,考虑如何提高批量求逆的效率。
不失一般性的,我们假设需要求逆的数组长度为 4:$[a,b,c,d]$,最终要求输出 $[a^{-1},b^{-1},c^{-1},d^{-1}]$。下面将以此为例来介绍 curve25519-dalek 库中的 batch-invert 算法。
```rust
pub fn batch_invert(inputs: &mut [Scalar]) -> Scalar {
use zeroize::Zeroizing;
let n = inputs.len();
let one: UnpackedScalar = Scalar::one().unpack().to_montgomery();
let scratch_vec = vec![one; n];
let mut scratch = Zeroizing::new(scratch_vec);
let mut acc = Scalar::one().unpack().to_montgomery();
for (input, scratch) in inputs.iter_mut().zip(scratch.iter_mut()) {
*scratch = acc;
let tmp = input.unpack().to_montgomery();
*input = tmp.pack();
acc = UnpackedScalar::montgomery_mul(&acc, &tmp);
}
debug_assert!(acc.pack() != Scalar::zero());
acc = acc.montgomery_invert().from_montgomery();
let ret = acc.pack();
for (input, scratch) in inputs.iter_mut().rev().zip(scratch.iter().rev()) {
let tmp = UnpackedScalar::montgomery_mul(&acc, &input.unpack());
*input = UnpackedScalar::montgomery_mul(&acc, &scratch).pack();
acc = tmp;
}
ret
}
```
首先,生成了与 scalar 数组长度相同的数组 scratch 以及赋值为 1 的 acc 变量。
随后在`---1---`和`---2---`的过程中,将 acc 保存在 scratch 相应位置上,并将每一个元素与 acc 累积相乘来更新 acc。以输入 $[a,b,c,d]$ 为例,经过这一步,得到的 scratch 被更新为 $[1,a,ab,abc]$,并且 $acc=abcd$。
在第`---3---`步之后,计算了 acc 的乘法逆,在计算过程中先将 acc 转成 montgomery 形式,然后在该形式下求逆,最终再转成原来的 scalar 形式。也就是,这步执行完后,$acc=(abcd)^{-1}$。
第四步则对 inputs 和 scratch 进行逆向迭代:$[d,c,b,a]$ & $[abc,ab,a,1]$,首先用 acc 与 scratch 中的当前元素做乘积,然后将 acc 与 inputs 中的当前元素相乘来更新 acc 的值。下面我们将每一步的迭代结果显示如下:
- i=0 :
- Inputs: $[d,c,b,a]$ -----> $inputs[0]=acc*scratch[0]=(abcd)^{-1}*abc=d^{-1}$ ---> inputs: $[d^{-1},c,b,a]$
- Scratch: $[abc,ab,a,1]$
- $acc=(abcd)^{-1}$ ----> $acc=acc*inputs[0]$ -----> $acc=(abc)^{-1}$
- i=1 :
- Inputs: $[d^{-1},c,b,a]$ -----> $inputs[1]=acc*scratch[1]=(abc)^{-1}*ab=c^{-1}$ ---> inputs: $[d^{-1},c^{-1},b,a]$
- Scratch: $[abc,ab,a,1]$
- $acc=(abc)^{-1}$ ----> $acc=acc*inputs[1]$ -----> $acc=(ab)^{-1}$
- i=2 :
- Inputs: $[d^{-1},c^{-1},b,a]$ -----> $inputs[2]=acc*scratch[2]=(ab)^{-1}*a=b^{-1}$ ---> inputs: $[d^{-1},c^{-1},b^{-1},a]$
- Scratch: $[abc,ab,a,1]$
- $acc=(ab)^{-1}$ ----> $acc=acc*inputs[2]$ -----> $acc=(a)^{-1}$
- i=3:
- Inputs: $[d^{-1},c^{-1},b^{-1},a]$ -----> $inputs[3]=acc*scratch[3]=(a)^{-1}*1=a^{-1}$ ---> inputs: $[d^{-1},c^{-1},b^{-1},a^{-1}]$
- Scratch: $[abc,ab,a,1]$
- $acc=(a)^{-1}$ ----> $acc=acc*inputs[3]$ -----> $acc=1$
可以看到,该算法总共只进行了一次求逆运算和 3n(n 为需要求逆的 scalar 数组长度)乘法运算。其中注释中标注"---1----"的位置需要 n 次乘法,标注"---2---"的位置需要 2n 次乘法。而之前对数组元素单独求逆则需要 n 次求逆运算。
批量求逆运算可以用在批量解码 Ed25519 点时的求逆运算:由于 Ed25519 椭圆曲线群上点的 256 比特编码只包含了 255 比特的 y 坐标和 1 比特 x 的正负性,因此在由 y 坐标计算 x 坐标时会涉及到求逆运算。这是如果批量地对点进行解码,就可以使用该算法来提高效率。另外,由于 ECDSA 签名机制在验证时需要求逆运算,也可以利用该算法在批量验证签名时加速执行效率。
## Effiency
利用 curve25519-dalek 库中自带的 benchmark 工具,对比这两种方法对于批量求逆的效率如下:
| #scalar | BatchInvert | Total/us | Average/us |
| ------- | ----------- | -------- | ---------- |
| 1 | False | 11.234 | 11.234 |
| 1 | True | 11.865 | 11.865 |
| 2 | True | 12.269 | 6.1345 |
| 4 | True | 12.676 | 3.169 |
| 8 | True | 13.445 | 1.6806 |
| 16 | True | 15.484 | 0.96775 |
可以看到,BatchInvert 在计算的 scalar 长度大于 1 时相比单个 invert 方法有较大的优势,并且随着`n=#scalar`的增加,效率优势越来越大。这是因为 1 次求逆运算的成本被均摊到 n 个 scalar 上面,最终随着 n 的增大,对于批量求逆运算,单个 scalar 求逆的计算复杂度会逐渐逼近 3 次乘法运算。
* 本文由 CoinEx Chain 开发团队撰写。CoinEx Chain 是全球首条基于 Tendermint 共识协议和 Cosmos SDK 开发的 DEX 专用公链,借助 IBC 来实现 DEX 公链、智能合约链、隐私链三条链合一的方式去解决可扩展性(Scalability)、去中心化(Decentralization)、安全性(security)区块链不可能三角的问题,能够高性能的支持数字资产的交易以及基于智能合约的 Defi 应用。